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
72 changes: 71 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://app.axiom.co/settings/profile), then export them as environment variables `NEXT_PUBLIC_AXIOM_DATASET` and `NEXT_PUBLIC_AXIOM_TOKEN`.

## Usage

## Capture traffic requests

Create or edit the `middleware.ts` in the root directory 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' });
logger.middleware(request)

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


export const config = {
}
```

### Web Vitals

Expand Down Expand Up @@ -157,6 +178,55 @@ const logger = new Logger({
console.log(event.message);
},
});


### 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 or edit the `error.tsx` file under your `/app` directory. Inside your component function use the logger to ingest the error to Axiom.

Example:

```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>
Ops! An Error has occurred:{" "}
</div>
);
}
```
dasfmi marked this conversation as resolved.
Show resolved Hide resolved

## Upgrade to the App Router
Expand Down
38 changes: 38 additions & 0 deletions examples/logger/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"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>
Ops! An Error has occurred:{" "}
</div>
);
}
14 changes: 14 additions & 0 deletions examples/logger/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
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' });
logger.middleware(request)

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

export const config = {
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "next-axiom",
"description": "Send WebVitals from your Next.js project to Axiom.",
"version": "1.2.0",
"version": "1.3.0",
"author": "Axiom, Inc.",
"license": "MIT",
"contributors": [
Expand Down
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
66 changes: 58 additions & 8 deletions src/logger.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
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';

Expand All @@ -12,9 +13,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 +33,7 @@ export enum LogLevel {

export interface RequestReport {
startTime: number;
endTime: number;
statusCode?: number;
ip?: string | null;
region?: string | null;
Expand All @@ -35,13 +42,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 @@ -60,7 +74,7 @@ export class Logger {
public logLevel: LogLevel = LogLevel.debug;
public config: LoggerConfig = {
autoFlush: true,
source: 'frontend',
source: 'frontend-log',
prettyPrint: prettyPrint,
};

Expand Down Expand Up @@ -98,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 @@ -130,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 @@ -161,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 @@ -190,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
Loading