From e16b244ab8f646e6d2ee7b410c4f2c04752a125c Mon Sep 17 00:00:00 2001 From: gabrielelpidio Date: Tue, 12 Nov 2024 03:41:23 -0400 Subject: [PATCH 1/9] feat: Add `details` option to withAxiomRouteHandler --- src/logger.ts | 2 + src/withAxiom.ts | 175 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 176 insertions(+), 1 deletion(-) diff --git a/src/logger.ts b/src/logger.ts index 3d215e48..53e74ab8 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -2,6 +2,7 @@ import { NextRequest } from 'next/server'; import { config, isBrowser, isVercelIntegration, Version } from './config'; import { NetlifyInfo } from './platform/netlify'; import { isNoPrettyPrint, throttle } from './shared'; +import { RequestJSON } from './withAxiom'; const url = config.getLogsEndpoint(); @@ -43,6 +44,7 @@ export interface RequestReport { scheme: string; userAgent?: string | null; durationMs?: number; + details?: RequestJSON; } export interface PlatformInfo { diff --git a/src/withAxiom.ts b/src/withAxiom.ts index b0313283..56d572a7 100644 --- a/src/withAxiom.ts +++ b/src/withAxiom.ts @@ -48,13 +48,177 @@ export function withAxiomNextConfig(nextConfig: NextConfig): NextConfig { }; } +export interface RequestJSON { + method: string; + url: string; + headers: Record; + params: Record; + cookies: Record; + nextUrl?: { + basePath: string; + buildId?: string; + defaultLocale?: string; + domainLocale?: { + defaultLocale: string; + domain: string; + locales?: string[]; + }; + hash: string; + host: string; + hostname: string; + href: string; + locale?: string; + origin: string; + password: string; + pathname: string; + port: string; + protocol: string; + search: string; + searchParams: Record; + username: string; + }; + ip?: string; + geo?: { + city?: string; + country?: string; + region?: string; + latitude?: string; + longitude?: string; + }; + body?: any; + cache: { + mode: RequestCache; + credentials: RequestCredentials; + redirect: RequestRedirect; + referrerPolicy: ReferrerPolicy; + integrity: string; + }; + mode: RequestMode; + destination: RequestDestination; + referrer: string; + keepalive: boolean; + signal: { + aborted: boolean; + reason: any; + }; +} + +/** + * Transforms a Next.js Request object into a JSON-serializable object + */ +export async function requestToJSON(request: Request | NextRequest): Promise { + // Get all headers + const headers: Record = {}; + request.headers.forEach((value, key) => { + headers[key] = value; + }); + + const url = new URL(request.url); + const params: Record = {}; + url.searchParams.forEach((value, key) => { + params[key] = value; + }); + + let cookiesData: Record = {}; + if (request instanceof NextRequest) { + request.cookies.getAll().forEach((cookie) => { + cookiesData[cookie.name] = cookie.value; + }); + } else { + const cookieHeader = headers['cookie']; + if (cookieHeader) { + cookiesData = Object.fromEntries( + cookieHeader.split(';').map((cookie) => { + const [key, value] = cookie.trim().split('='); + return [key, value]; + }) + ); + } + } + + let nextUrlData: RequestJSON['nextUrl'] | undefined; + if (request instanceof NextRequest) { + const nextUrl = request.nextUrl; + nextUrlData = { + basePath: nextUrl.basePath, + buildId: nextUrl.buildId, + hash: nextUrl.hash, + host: nextUrl.host, + hostname: nextUrl.hostname, + href: nextUrl.href, + origin: nextUrl.origin, + password: nextUrl.password, + pathname: nextUrl.pathname, + port: nextUrl.port, + protocol: nextUrl.protocol, + search: nextUrl.search, + searchParams: Object.fromEntries(nextUrl.searchParams.entries()), + username: nextUrl.username, + }; + } + + let body: RequestJSON['body'] | undefined; + if (request.body) { + try { + const clonedRequest = request.clone(); + try { + body = await clonedRequest.json(); + } catch { + body = await clonedRequest.text(); + } + } catch (error) { + console.warn('Could not parse request body:', error); + } + } + + const cache: RequestJSON['cache'] = { + mode: request.cache, + credentials: request.credentials, + redirect: request.redirect, + referrerPolicy: request.referrerPolicy, + integrity: request.integrity, + }; + + let geoData: Pick | undefined; + if (request instanceof NextRequest) { + geoData = { + ip: request.ip, + geo: request.geo, + }; + } + + return { + method: request.method, + url: request.url, + headers, + params, + cookies: cookiesData, + nextUrl: nextUrlData, + ...geoData, + body, + cache, + mode: request.mode, + destination: request.destination, + referrer: request.referrer, + keepalive: request.keepalive, + signal: { + aborted: request.signal.aborted, + reason: request.signal.reason, + }, + }; +} + export type AxiomRequest = NextRequest & { log: Logger }; type NextHandler = ( req: AxiomRequest, arg?: T ) => Promise | Promise | NextResponse | Response; -export function withAxiomRouteHandler(handler: NextHandler): NextHandler { +type AxiomRouteHandlerConfig = { + details?: boolean | (keyof RequestJSON)[]; +}; + +export function withAxiomRouteHandler(handler: NextHandler, config?: AxiomRouteHandlerConfig): NextHandler { return async (req: Request | NextRequest, arg: any) => { let region = ''; if ('geo' in req) { @@ -69,6 +233,8 @@ export function withAxiomRouteHandler(handler: NextHandler): NextHandler { pathname = new URL(req.url).pathname; } + const details = Array.isArray(config?.details) || config?.details === true ? await requestToJSON(req) : undefined; + const report: RequestReport = { startTime: new Date().getTime(), endTime: new Date().getTime(), @@ -79,6 +245,13 @@ export function withAxiomRouteHandler(handler: NextHandler): NextHandler { scheme: req.url.split('://')[0], ip: req.headers.get('x-forwarded-for'), region, + details: Array.isArray(config?.details) + ? (Object.fromEntries( + Object.entries(details as RequestJSON).filter(([key]) => + (config?.details as (keyof RequestJSON)[]).includes(key as keyof RequestJSON) + ) + ) as RequestJSON) + : details, }; // main logger, mainly used to log reporting on the incoming HTTP request From c44ca8fc7e6578e0e46dcf7508bd27b93de5ca5e Mon Sep 17 00:00:00 2001 From: gabrielelpidio Date: Tue, 12 Nov 2024 13:52:10 -0400 Subject: [PATCH 2/9] fix: Add AxiomRouteHandlerConfig to withAxiom overload --- src/withAxiom.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/withAxiom.ts b/src/withAxiom.ts index 56d572a7..9a782fb8 100644 --- a/src/withAxiom.ts +++ b/src/withAxiom.ts @@ -321,14 +321,14 @@ function isNextConfig(param: WithAxiomParam): param is NextConfig { // withAxiom can be called either with NextConfig, which will add proxy rewrites // to improve deliverability of Web-Vitals and logs. -export function withAxiom(param: NextHandler): NextHandler; +export function withAxiom(param: NextHandler, config?: AxiomRouteHandlerConfig): NextHandler; export function withAxiom(param: NextConfig): NextConfig; -export function withAxiom(param: WithAxiomParam) { +export function withAxiom(param: WithAxiomParam, config?: AxiomRouteHandlerConfig) { if (typeof param == 'function') { - return withAxiomRouteHandler(param); + return withAxiomRouteHandler(param, config); } else if (isNextConfig(param)) { return withAxiomNextConfig(param); } - return withAxiomRouteHandler(param); + return withAxiomRouteHandler(param, config); } From 7047c130f56023f5a00875d6fa46f9270e2ee7ce Mon Sep 17 00:00:00 2001 From: gabrielelpidio Date: Tue, 12 Nov 2024 15:20:31 -0400 Subject: [PATCH 3/9] docs: Update README to include configuration options for route handlers --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 5e3f89b0..34fee52b 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,19 @@ export const GET = withAxiom((req: AxiomRequest) => { }); ``` +Route handlers also accept a configuration object as the second argument. This object can contain the following properties: + +- `details`: It accepts a boolean or an array of keys. If pass `true`, it will add the request details to the log (method, URL, headers, etc.). If you pass an array of strings, it will only log the specified keys. + +```ts +export const GET = withAxiom( + async () => { + return new Response("Hello World!"); + }, + { details: ['body', 'nextUrl'] } // { details: true } is also valid +); +``` + ### Client components To send logs from client components, add `useLogger` from next-axiom to your component: From 295b7ad72b4f96d6cae176cdb8f413fef951826c Mon Sep 17 00:00:00 2001 From: gabrielelpidio Date: Mon, 18 Nov 2024 13:15:33 -0400 Subject: [PATCH 4/9] docs: documentation fixes --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 34fee52b..b954444c 100644 --- a/README.md +++ b/README.md @@ -121,9 +121,9 @@ export const GET = withAxiom((req: AxiomRequest) => { }); ``` -Route handlers also accept a configuration object as the second argument. This object can contain the following properties: +Route handlers accept a configuration object as the second argument. This object can contain the following properties: -- `details`: It accepts a boolean or an array of keys. If pass `true`, it will add the request details to the log (method, URL, headers, etc.). If you pass an array of strings, it will only log the specified keys. +- `details`: Accepts a boolean or an array of keys. If you pass `true`, it will add the request details to the log (method, URL, headers, etc.). If you pass an array of strings, it will only add the specified keys. See [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request/url) and [NextRequest](https://nextjs.org/docs/app/api-reference/functions/next-request) for documentation on the available keys. ```ts export const GET = withAxiom( From 383cfae0c3f394428e5f2a8c91b038f3aca66fa8 Mon Sep 17 00:00:00 2001 From: gabrielelpidio Date: Mon, 18 Nov 2024 13:17:01 -0400 Subject: [PATCH 5/9] chore: Rename 'details' to 'logReq' --- README.md | 4 ++-- src/logger.ts | 2 +- src/withAxiom.ts | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index b954444c..4d8b6b80 100644 --- a/README.md +++ b/README.md @@ -123,14 +123,14 @@ export const GET = withAxiom((req: AxiomRequest) => { Route handlers accept a configuration object as the second argument. This object can contain the following properties: -- `details`: Accepts a boolean or an array of keys. If you pass `true`, it will add the request details to the log (method, URL, headers, etc.). If you pass an array of strings, it will only add the specified keys. See [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request/url) and [NextRequest](https://nextjs.org/docs/app/api-reference/functions/next-request) for documentation on the available keys. +- `logReq`: Accepts a boolean or an array of keys. If you pass `true`, it will add the request details to the log (method, URL, headers, etc.). If you pass an array of strings, it will only add the specified keys. See [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request/url) and [NextRequest](https://nextjs.org/docs/app/api-reference/functions/next-request) for documentation on the available keys. ```ts export const GET = withAxiom( async () => { return new Response("Hello World!"); }, - { details: ['body', 'nextUrl'] } // { details: true } is also valid + { logReq: ['body', 'nextUrl'] } // { logReq: true } is also valid ); ``` diff --git a/src/logger.ts b/src/logger.ts index 53e74ab8..88ff91d9 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -44,7 +44,7 @@ export interface RequestReport { scheme: string; userAgent?: string | null; durationMs?: number; - details?: RequestJSON; + logReq?: RequestJSON; } export interface PlatformInfo { diff --git a/src/withAxiom.ts b/src/withAxiom.ts index 9a782fb8..0935edb0 100644 --- a/src/withAxiom.ts +++ b/src/withAxiom.ts @@ -215,7 +215,7 @@ type NextHandler = ( ) => Promise | Promise | NextResponse | Response; type AxiomRouteHandlerConfig = { - details?: boolean | (keyof RequestJSON)[]; + logReq?: boolean | (keyof RequestJSON)[]; }; export function withAxiomRouteHandler(handler: NextHandler, config?: AxiomRouteHandlerConfig): NextHandler { @@ -233,7 +233,7 @@ export function withAxiomRouteHandler(handler: NextHandler, config?: AxiomRouteH pathname = new URL(req.url).pathname; } - const details = Array.isArray(config?.details) || config?.details === true ? await requestToJSON(req) : undefined; + const logReq = Array.isArray(config?.logReq) || config?.logReq === true ? await requestToJSON(req) : undefined; const report: RequestReport = { startTime: new Date().getTime(), @@ -245,13 +245,13 @@ export function withAxiomRouteHandler(handler: NextHandler, config?: AxiomRouteH scheme: req.url.split('://')[0], ip: req.headers.get('x-forwarded-for'), region, - details: Array.isArray(config?.details) + logReq: Array.isArray(config?.logReq) ? (Object.fromEntries( - Object.entries(details as RequestJSON).filter(([key]) => - (config?.details as (keyof RequestJSON)[]).includes(key as keyof RequestJSON) + Object.entries(logReq as RequestJSON).filter(([key]) => + (config?.logReq as (keyof RequestJSON)[]).includes(key as keyof RequestJSON) ) ) as RequestJSON) - : details, + : logReq, }; // main logger, mainly used to log reporting on the incoming HTTP request From 83f0715281d662823ea69d22aa772868eff8e9af Mon Sep 17 00:00:00 2001 From: gabrielelpidio Date: Tue, 19 Nov 2024 23:20:25 -0400 Subject: [PATCH 6/9] refactor: Update request type checks to use property existence instead of instanceof --- src/withAxiom.ts | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/withAxiom.ts b/src/withAxiom.ts index 0935edb0..56078294 100644 --- a/src/withAxiom.ts +++ b/src/withAxiom.ts @@ -2,7 +2,7 @@ import { NextConfig } from 'next'; import { Rewrite } from 'next/dist/lib/load-custom-routes'; import { config, isEdgeRuntime, isVercelIntegration } from './config'; import { LogLevel, Logger, RequestReport } from './logger'; -import { NextRequest, type NextResponse } from 'next/server'; +import { type NextRequest, type NextResponse } from 'next/server'; import { EndpointType } from './shared'; export function withAxiomNextConfig(nextConfig: NextConfig): NextConfig { @@ -120,7 +120,7 @@ export async function requestToJSON(request: Request | NextRequest): Promise = {}; - if (request instanceof NextRequest) { + if ('cookies' in request) { request.cookies.getAll().forEach((cookie) => { cookiesData[cookie.name] = cookie.value; }); @@ -137,7 +137,7 @@ export async function requestToJSON(request: Request | NextRequest): Promise | undefined; - if (request instanceof NextRequest) { - geoData = { - ip: request.ip, - geo: request.geo, - }; + let ip: string | undefined; + if ('ip' in request) { + ip = request.ip; + } + + let geo: NextRequest['geo'] | undefined; + if ('geo' in request) { + geo = request.geo; } return { @@ -194,7 +196,8 @@ export async function requestToJSON(request: Request | NextRequest): Promise Date: Tue, 19 Nov 2024 23:57:53 -0400 Subject: [PATCH 7/9] refactor: Remove 'params' from RequestJSON --- src/withAxiom.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/withAxiom.ts b/src/withAxiom.ts index 56078294..7e3e064e 100644 --- a/src/withAxiom.ts +++ b/src/withAxiom.ts @@ -52,7 +52,6 @@ export interface RequestJSON { method: string; url: string; headers: Record; - params: Record; cookies: Record; nextUrl?: { basePath: string; @@ -113,12 +112,6 @@ export async function requestToJSON(request: Request | NextRequest): Promise = {}; - url.searchParams.forEach((value, key) => { - params[key] = value; - }); - let cookiesData: Record = {}; if ('cookies' in request) { request.cookies.getAll().forEach((cookie) => { @@ -193,7 +186,6 @@ export async function requestToJSON(request: Request | NextRequest): Promise Date: Wed, 20 Nov 2024 01:21:50 -0400 Subject: [PATCH 8/9] refactor: Rename 'logReq' to 'requestDetails' in ingest object --- src/logger.ts | 2 +- src/withAxiom.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/logger.ts b/src/logger.ts index 88ff91d9..0f9a7d46 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -44,7 +44,7 @@ export interface RequestReport { scheme: string; userAgent?: string | null; durationMs?: number; - logReq?: RequestJSON; + requestDetails?: RequestJSON; } export interface PlatformInfo { diff --git a/src/withAxiom.ts b/src/withAxiom.ts index 7e3e064e..97f2695e 100644 --- a/src/withAxiom.ts +++ b/src/withAxiom.ts @@ -240,7 +240,7 @@ export function withAxiomRouteHandler(handler: NextHandler, config?: AxiomRouteH scheme: req.url.split('://')[0], ip: req.headers.get('x-forwarded-for'), region, - logReq: Array.isArray(config?.logReq) + requestDetails: Array.isArray(config?.logReq) ? (Object.fromEntries( Object.entries(logReq as RequestJSON).filter(([key]) => (config?.logReq as (keyof RequestJSON)[]).includes(key as keyof RequestJSON) From 34d6e68854882819c7180cee64c9a1183124f322 Mon Sep 17 00:00:00 2001 From: gabrielelpidio Date: Thu, 21 Nov 2024 13:23:28 -0400 Subject: [PATCH 9/9] refactor: Rename 'logReq' to 'logRequestDetails' and 'requestDetails' to 'details' in RequestReport --- README.md | 4 ++-- src/logger.ts | 2 +- src/withAxiom.ts | 15 +++++++++------ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 4d8b6b80..0afb2716 100644 --- a/README.md +++ b/README.md @@ -123,14 +123,14 @@ export const GET = withAxiom((req: AxiomRequest) => { Route handlers accept a configuration object as the second argument. This object can contain the following properties: -- `logReq`: Accepts a boolean or an array of keys. If you pass `true`, it will add the request details to the log (method, URL, headers, etc.). If you pass an array of strings, it will only add the specified keys. See [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request/url) and [NextRequest](https://nextjs.org/docs/app/api-reference/functions/next-request) for documentation on the available keys. +- `logRequestDetails`: Accepts a boolean or an array of keys. If you pass `true`, it will add the request details to the log (method, URL, headers, etc.). If you pass an array of strings, it will only add the specified keys. See [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request/url) and [NextRequest](https://nextjs.org/docs/app/api-reference/functions/next-request) for documentation on the available keys. ```ts export const GET = withAxiom( async () => { return new Response("Hello World!"); }, - { logReq: ['body', 'nextUrl'] } // { logReq: true } is also valid + { logRequestDetails: ['body', 'nextUrl'] } // { logRequestDetails: true } is also valid ); ``` diff --git a/src/logger.ts b/src/logger.ts index 0f9a7d46..53e74ab8 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -44,7 +44,7 @@ export interface RequestReport { scheme: string; userAgent?: string | null; durationMs?: number; - requestDetails?: RequestJSON; + details?: RequestJSON; } export interface PlatformInfo { diff --git a/src/withAxiom.ts b/src/withAxiom.ts index 97f2695e..34fa3b93 100644 --- a/src/withAxiom.ts +++ b/src/withAxiom.ts @@ -210,7 +210,7 @@ type NextHandler = ( ) => Promise | Promise | NextResponse | Response; type AxiomRouteHandlerConfig = { - logReq?: boolean | (keyof RequestJSON)[]; + logRequestDetails?: boolean | (keyof RequestJSON)[]; }; export function withAxiomRouteHandler(handler: NextHandler, config?: AxiomRouteHandlerConfig): NextHandler { @@ -228,7 +228,10 @@ export function withAxiomRouteHandler(handler: NextHandler, config?: AxiomRouteH pathname = new URL(req.url).pathname; } - const logReq = Array.isArray(config?.logReq) || config?.logReq === true ? await requestToJSON(req) : undefined; + const requestDetails = + Array.isArray(config?.logRequestDetails) || config?.logRequestDetails === true + ? await requestToJSON(req) + : undefined; const report: RequestReport = { startTime: new Date().getTime(), @@ -240,13 +243,13 @@ export function withAxiomRouteHandler(handler: NextHandler, config?: AxiomRouteH scheme: req.url.split('://')[0], ip: req.headers.get('x-forwarded-for'), region, - requestDetails: Array.isArray(config?.logReq) + details: Array.isArray(config?.logRequestDetails) ? (Object.fromEntries( - Object.entries(logReq as RequestJSON).filter(([key]) => - (config?.logReq as (keyof RequestJSON)[]).includes(key as keyof RequestJSON) + Object.entries(requestDetails as RequestJSON).filter(([key]) => + (config?.logRequestDetails as (keyof RequestJSON)[]).includes(key as keyof RequestJSON) ) ) as RequestJSON) - : logReq, + : requestDetails, }; // main logger, mainly used to log reporting on the incoming HTTP request