Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve traffic and error capturing #193

Merged
merged 14 commits into from
May 26, 2024
75 changes: 74 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,28 @@ no further configuration is required.

Otherwise create a dataset and an API token in [Axiom settings](https://cloud.axiom.co/settings/profile), then export them as environment variables `NEXT_PUBLIC_AXIOM_DATASET` and `NEXT_PUBLIC_AXIOM_TOKEN`.

## Usage

## Capture traffic requests

Create a `middleware.ts` in the root dir of your app:

```typescript
import { Logger } from 'next-axiom'
import { NextResponse } from 'next/server'
import type { NextFetchEvent, NextRequest } from 'next/server'

export async function middleware(request: NextRequest, event: NextFetchEvent) {
const logger = new Logger({ source: 'middleware' }); // traffic, request
schehata marked this conversation as resolved.
Show resolved Hide resolved
logger.middleware(request)

event.waitUntil(logger.flush())
return NextResponse.next()
schehata marked this conversation as resolved.
Show resolved Hide resolved


// See "Matching Paths" below to learn more
schehata marked this conversation as resolved.
Show resolved Hide resolved
export const config = {
}
```

### Web Vitals

Expand Down Expand Up @@ -146,6 +167,58 @@ export NEXT_PUBLIC_AXIOM_LOG_LEVEL=info

You can also disable logging completely by setting the log level to `off`.


### Capturing Errors

To capture routing errors we can use the [Error Handling](https://nextjs.org/docs/app/building-your-application/routing/error-handling) mechanism of Next.

Create a new file named `error.tsx` under your `/app` directory. Inside your component function use the logger to ingest the error to Axiom. e.g:
schehata marked this conversation as resolved.
Show resolved Hide resolved

```typescript
"use client";

import NavTable from "@/components/NavTable";
import { LogLevel } from "@/next-axiom/logger";
import { useLogger } from "next-axiom";
import { usePathname } from "next/navigation";

export default function ErrorPage({
error,
}: {
error: Error & { digest?: string };
}) {
const pathname = usePathname()
const log = useLogger({ source: "error.tsx" });
let status = error.message == 'Invalid URL' ? 404 : 500;

log.logHttpRequest(
LogLevel.error,
error.message,
{
host: window.location.href,
path: pathname,
statusCode: status,
},
{
error: error.name,
cause: error.cause,
stack: error.stack,
digest: error.digest,
},
);

return (
<div className="p-8">
Ops! An Error has occurred:{" "}
<p className="text-red-400 px-8 py-2 text-lg">`{error.message}`</p>
<div className="w-1/3 mt-8">
<NavTable />
</div>
</div>
);
}
```
schehata marked this conversation as resolved.
Show resolved Hide resolved

## Upgrade to the App Router

next-axiom switched to support the App Router starting with version 1.0. If you are upgrading a Pages Router app with next-axiom v0.x to the App Router, you will need to make the following changes:
Expand Down
42 changes: 42 additions & 0 deletions examples/logger/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"use client";

import NavTable from "@/components/NavTable";
import { LogLevel } from "@/next-axiom/logger";
import { useLogger } from "next-axiom";
import { usePathname } from "next/navigation";

export default function ErrorPage({
error,
}: {
error: Error & { digest?: string };
}) {
const pathname = usePathname()
const log = useLogger({ source: "error.tsx" });
let status = error.message == 'Invalid URL' ? 404 : 500;

log.logHttpRequest(
LogLevel.error,
error.message,
{
host: window.location.href,
path: pathname,
statusCode: status,
},
{
error: error.name,
cause: error.cause,
stack: error.stack,
digest: error.digest,
},
);

return (
<div className="p-8">
Ops! An Error has occurred:{" "}
<p className="text-red-400 px-8 py-2 text-lg">`{error.message}`</p>
<div className="w-1/3 mt-8">
<NavTable />
</div>
</div>
);
}
15 changes: 15 additions & 0 deletions examples/logger/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Logger } from 'next-axiom'
import { NextResponse } from 'next/server'
import type { NextFetchEvent, NextRequest } from 'next/server'

export async function middleware(request: NextRequest, event: NextFetchEvent) {
const logger = new Logger({ source: 'middleware' }); // traffic, request
logger.middleware(request)

event.waitUntil(logger.flush())
return NextResponse.next()
}

// See "Matching Paths" below to learn more
export const config = {
}
7 changes: 5 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ declare global {
}

export const Version = require('../package.json').version;
export const isVercel = process.env.NEXT_PUBLIC_AXIOM_INGEST_ENDPOINT || process.env.AXIOM_INGEST_ENDPOINT;
// detect if Vercel integration & logdrain is enabled
export const isVercelIntegration = process.env.NEXT_PUBLIC_AXIOM_INGEST_ENDPOINT || process.env.AXIOM_INGEST_ENDPOINT;
// detect if app is running on the Vercel platform
export const isVercel = process.env.NEXT_PUBLIC_VERCEL || process.env.VERCEL;
export const isNetlify = process.env.NETLIFY == 'true';
export const isWebWorker =
typeof self !== 'undefined' &&
Expand All @@ -20,7 +23,7 @@ export const isEdgeRuntime = globalThis.EdgeRuntime ? true : false;
// Detect the platform provider, and return the appropriate config
// fallback to generic config if no provider is detected
let config = new GenericConfig();
if (isVercel) {
if (isVercelIntegration) {
config = new VercelConfig();
} else if (isNetlify) {
config = new NetlifyConfig();
Expand Down
68 changes: 60 additions & 8 deletions src/logger.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { config, isBrowser, isVercel, Version } from './config';
import { NextRequest } from 'next/server';
import { config, isBrowser, isVercelIntegration, Version } from './config';
import { NetlifyInfo } from './platform/netlify';
import { isNoPrettyPrint, throttle } from './shared';



const url = config.getLogsEndpoint();

const LOG_LEVEL = process.env.NEXT_PUBLIC_AXIOM_LOG_LEVEL || 'debug';
Expand All @@ -12,9 +15,14 @@ export interface LogEvent {
fields: any;
_time: string;
request?: RequestReport;
git?: any,
source: string;
platform?: PlatformInfo;
vercel?: PlatformInfo;
netlify?: NetlifyInfo;
"@app": {
"next-axiom-version": string;
}
}

export enum LogLevel {
Expand All @@ -27,6 +35,7 @@ export enum LogLevel {

export interface RequestReport {
startTime: number;
endTime: number;
statusCode?: number;
ip?: string | null;
region?: string | null;
Expand All @@ -35,13 +44,20 @@ export interface RequestReport {
method: string;
scheme: string;
userAgent?: string | null;
durationMs?: number;
}

export interface PlatformInfo {
environment?: string;
region?: string;
route?: string;
source?: string;
deploymentId?: string;
deploymentUrl?: string;
commit?: string;
project?: string;
repo?: string;
ref?: string;
}

export type LoggerConfig = {
Expand All @@ -59,7 +75,7 @@ export class Logger {
public logLevel: LogLevel = LogLevel.debug;
public config: LoggerConfig = {
autoFlush: true,
source: 'frontend',
source: 'frontend-log',
};

constructor(public initConfig: LoggerConfig = {}) {
Expand Down Expand Up @@ -96,15 +112,16 @@ export class Logger {
return new Logger({ ...this.config, req: { ...this.config.req, ...req } });
};

_log = (level: LogLevel, message: string, args: { [key: string]: any } = {}) => {
if (level < this.logLevel) {
return;
}
private _transformEvent = (level: LogLevel, message: string, args: { [key: string]: any } = {}) => {
const logEvent: LogEvent = {
level: LogLevel[level].toString(),
message,
_time: new Date(Date.now()).toISOString(),
source: this.config.source!,
fields: this.config.args || {},
"@app": {
"next-axiom-version": Version,
}
};

// check if passed args is an object, if its not an object, add it to fields.args
Expand All @@ -128,6 +145,41 @@ export class Logger {
}
}

return logEvent;
}

logHttpRequest(level: LogLevel, message: string, request: any, args: any) {
const logEvent = this._transformEvent(level, message, args);
logEvent.request = request;
this.logEvents.push(logEvent);
if (this.config.autoFlush) {
this.throttledSendLogs();
}
}

middleware(request: NextRequest) {
const req = {
ip: request.ip,
region: request.geo?.region,
method: request.method,
host: request.nextUrl.hostname,
path: request.nextUrl.pathname,
scheme: request.nextUrl.protocol.split(":")[0],
referer: request.headers.get('Referer'),
userAgent: request.headers.get('user-agent'),
}

const message = `[${request.method}] [middleware: "middleware"] ${request.nextUrl.pathname}`

return this.logHttpRequest(LogLevel.info, message, req, {})
}

private _log = (level: LogLevel, message: string, args: { [key: string]: any } = {}) => {
if (level < this.logLevel) {
return;
}
const logEvent = this._transformEvent(level, message, args)

this.logEvents.push(logEvent);
if (this.config.autoFlush) {
this.throttledSendLogs();
Expand Down Expand Up @@ -159,7 +211,7 @@ export class Logger {
// if vercel integration is enabled, we can utilize the log drain
// to send logs to Axiom without HTTP.
// This saves resources and time on lambda and edge functions
if (isVercel && (this.config.source === 'edge' || this.config.source === 'lambda')) {
if (isVercelIntegration && (this.config.source === 'edge-log' || this.config.source === 'lambda-log')) {
this.logEvents.forEach((ev) => console.log(JSON.stringify(ev)));
this.logEvents = [];
return;
Expand Down Expand Up @@ -188,7 +240,7 @@ export class Logger {
if (typeof fetch === 'undefined') {
const fetch = await require('whatwg-fetch');
return fetch(url, reqOptions).catch(console.error);
} else if (isBrowser && isVercel && navigator.sendBeacon) {
} else if (isBrowser && isVercelIntegration && navigator.sendBeacon) {
// sendBeacon fails if message size is greater than 64kb, so
// we fall back to fetch.
// Navigator has to be bound to ensure it does not error in some browsers
Expand Down
2 changes: 0 additions & 2 deletions src/platform/base.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { NextWebVitalsMetric } from 'next/app';
import { RequestReport } from '../logger';
import { EndpointType } from '../shared';

// This is the base class for all platform providers. It contains all the different
Expand All @@ -16,7 +15,6 @@ export default interface Provider {
getIngestURL(t: EndpointType): string;
wrapWebVitalsObject(metrics: NextWebVitalsMetric[]): any;
injectPlatformMetadata(logEvent: any, source: string): void;
generateRequestMeta(req: any): RequestReport;
getLogsEndpoint(): string
getWebVitalsEndpoint(): string
}
Loading