Created
November 19, 2024 07:24
-
-
Save WomB0ComB0/52db9ce6a62989af4562314dd72da109 to your computer and use it in GitHub Desktop.
Must have exported Vercel logs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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