Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save WomB0ComB0/52db9ce6a62989af4562314dd72da109 to your computer and use it in GitHub Desktop.
Save WomB0ComB0/52db9ce6a62989af4562314dd72da109 to your computer and use it in GitHub Desktop.
Must have exported Vercel logs
/**
* Node.js file system module for file operations
*/
import fs from "node:fs";
/**
* Node.js path module for handling file paths
*/
import path from "node:path";
/**
* Utility type that flattens complex types into a simpler object type
* @template T - The type to prettify
*/
type Prettify<T> = {
[K in keyof T]: T[K];
} & {};
/**
* Type representing whitespace characters (tab, newline, space)
*/
type Whitespace = "\u{9}" | "\u{A}" | "\u{20}";
/**
* Recursively trims whitespace from the left side of a string type
* @template V - String type to trim
*/
type TrimLeft<V extends string> = V extends `${Whitespace}${infer R}`
? TrimLeft<R>
: V;
/**
* Recursively trims whitespace from the right side of a string type
* @template V - String type to trim
*/
type TrimRight<V extends string> = V extends `${infer R}${Whitespace}`
? TrimRight<R>
: V;
/**
* Trims whitespace from both sides of a string type
* @template V - String type to trim
*/
type Trim<V extends string> = TrimLeft<TrimRight<V>>;
/**
* Type guard to check if a string matches ISO date format (YYYY-MM-DD)
* @template T - Type to check
*/
type IsDate<T> = T extends string
? T extends `${number}${number}${number}${number}-${number}${number}-${number}${number}`
? true
: false
: false;
/**
* Type guard to check if a value is numeric (number or numeric string)
* @template T - Type to check
*/
type IsNumeric<T> = T extends number
? true
: T extends `${number}`
? true
: false;
/**
* Recursively extracts and transforms types from complex objects
* Converts date strings to Date objects and numeric strings to numbers
* @template T - Type to transform
*/
type ExtractType<T> = T extends Array<infer U>
? ExtractType<U>[]
: T extends object
? {
[K in keyof T]: T[K] extends string
? IsDate<T[K]> extends true
? Date
: IsNumeric<T[K]> extends true
? number
: string
: ExtractType<T[K]>;
}
: T extends string
? IsDate<T> extends true
? Date
: IsNumeric<T> extends true
? number
: string
: T extends number
? number
: T extends boolean
? boolean
: T extends null
? null
: T extends undefined
? undefined
: never;
/**
* Base type definition for a log entry
* Contains all the raw properties before type transformation
*/
type BaseLogEntry = {
TimeUTC: string;
timestampInMs: number;
requestPath: string;
responseStatusCode: string | number;
requestMethod: string;
requestId: string;
requestUserAgent: string[];
level: string;
environment: string;
branch: string;
vercelCache: string;
type: string;
function: string;
host: string;
deploymentDomain: string;
deploymentId: string;
lambdaDurationInMs: number;
lambdaRegion: string;
lambdaMaxMemoryUsed: number;
lambdaMemorySize: number;
message: string;
projectId: string;
wafAction: string;
};
/**
* Transformed log entry type with proper type conversions applied
* Uses ExtractType to convert date strings and numeric strings
*/
type LogEntry = ExtractType<Prettify<BaseLogEntry>>;
/**
* Checks if a value is numeric (number or numeric string)
* @param value - Value to check
* @returns True if the value is numeric, false otherwise
*/
const isNumeric = (value: unknown): value is string | number => {
if (typeof value === "number") return true;
if (typeof value === "string") {
return !isNaN(Number(value)) && !isNaN(parseFloat(value));
}
return false;
};
/**
* Checks if a string matches ISO date format (YYYY-MM-DD)
* @param value - Value to check
* @returns True if the value is a date string, false otherwise
*/
const isDateString = (value: unknown): value is string => {
if (typeof value !== "string") return false;
return /^\d{4}-\d{2}-\d{2}/.test(value);
};
/**
* Type guard to validate if an unknown object is a valid BaseLogEntry
* @param log - Object to validate
* @returns True if the object matches BaseLogEntry structure, false otherwise
*/
const isValidLogEntry = (log: unknown): log is BaseLogEntry => {
const typedLog = log as BaseLogEntry;
return (
typeof typedLog === "object" &&
typedLog !== null &&
typeof typedLog.requestPath === "string" &&
(typeof typedLog.responseStatusCode === "string" ||
typeof typedLog.responseStatusCode === "number") &&
typeof typedLog.TimeUTC === "string"
);
};
/**
* Transforms a BaseLogEntry into a LogEntry by converting types
* - Converts numeric strings to numbers
* - Converts date strings to Date objects
* @param log - Base log entry to transform
* @returns Transformed log entry with proper types
*/
const transformLogEntry = (log: BaseLogEntry): LogEntry => {
const transformed = { ...log };
if (
typeof transformed.responseStatusCode === "string" &&
isNumeric(transformed.responseStatusCode)
) {
transformed.responseStatusCode = Number(transformed.responseStatusCode);
}
if (
typeof transformed.TimeUTC === "string" &&
isDateString(transformed.TimeUTC)
) {
transformed.TimeUTC = new Date(transformed.TimeUTC).toISOString();
}
return transformed as LogEntry;
};
/**
* Processes a JSON file containing log entries, filters and transforms them
* @param inputPath - Path to input JSON file
* @param outputPath - Path where filtered results will be saved
* @param options - Processing options
* @param options.filterStatusCode - If true, only keeps entries with status code >= 400
* @param options.domainFilter - If provided, only keeps entries containing this domain
* @param options.debugMode - If true, logs detailed processing information
* @throws {Error} If JSON parsing fails or input is not an array
*/
const processLogs = async (
inputPath: string,
outputPath: string,
options: {
filterStatusCode?: boolean;
domainFilter?: string;
debugMode?: boolean;
} = {}
): Promise<void> => {
try {
const data = await fs.promises.readFile(inputPath, "utf8");
const parsedData = JSON.parse(data);
if (!Array.isArray(parsedData)) {
throw new Error("Input data must be an array");
}
let totalEntries = parsedData.length;
let invalidFormatCount = 0;
let domainMismatchCount = 0;
let statusCodeMismatchCount = 0;
const filteredData = parsedData
.filter((log): log is BaseLogEntry => {
if (!isValidLogEntry(log)) {
if (options.debugMode) {
console.log("Invalid log entry:", log);
}
invalidFormatCount++;
return false;
}
const domainMatch = options.domainFilter
? log.requestPath.includes(options.domainFilter)
: true;
if (!domainMatch) {
if (options.debugMode) {
console.log("Domain mismatch:", {
requestPath: log.requestPath,
expectedDomain: options.domainFilter
});
}
domainMismatchCount++;
return false;
}
const statusCodeMatch = options.filterStatusCode
? Number(log.responseStatusCode) >= 400
: true;
if (!statusCodeMatch) {
if (options.debugMode) {
console.log("Status code mismatch:", {
statusCode: log.responseStatusCode
});
}
statusCodeMismatchCount++;
return false;
}
return true;
})
.map(transformLogEntry);
await fs.promises.writeFile(
outputPath,
JSON.stringify(filteredData, null, 2),
"utf8"
);
console.log("\nProcessing Summary:");
console.log(`Total entries: ${totalEntries}`);
console.log(`Entries kept: ${filteredData.length}`);
console.log(`\nSkipped entries breakdown:`);
console.log(`- Invalid format: ${invalidFormatCount}`);
console.log(`- Domain mismatch: ${domainMismatchCount}`);
console.log(`- Status code < 400: ${statusCodeMismatchCount}`);
} catch (error) {
if (error instanceof SyntaxError) {
throw new Error("Failed to parse JSON data: " + error.message);
}
throw error;
}
};
/**
* Path to the input JSON file, relative to current working directory
*/
const INPUT_PATH = path.join(process.cwd(), `<input-file>.json`);
/**
* Path where the filtered output will be saved, relative to current working directory
*/
const OUTPUT_PATH = path.join(process.cwd(), `filtered-<input-file>.json`);
// Process logs with filtering options
processLogs(INPUT_PATH, OUTPUT_PATH, {
filterStatusCode: true,
domainFilter: "<domain>",
debugMode: true
}).catch((error) => {
console.error("Error processing logs:", error);
process.exit(1);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment