Skip to content

Commit

Permalink
Merge pull request #134 from axiomhq/next13
Browse files Browse the repository at this point in the history
Support Next.js 13
  • Loading branch information
bahlo committed Jul 24, 2023
2 parents e6648d2 + 91a28de commit fa3a2c8
Show file tree
Hide file tree
Showing 48 changed files with 8,677 additions and 7,701 deletions.
11 changes: 5 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,17 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 17
node-version: 18
- run: npm install
- run: npm run check-format
unittests:
name: Check Tests
runs-on: ubuntu-latest
env:
AXIOM_INGEST_ENDPOINT: https://example.co/api/test
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 17
node-version: 18
- run: npm install
- run: npm test
build:
Expand All @@ -39,9 +37,10 @@ jobs:
strategy:
matrix:
node:
- 15.x
- 16.x
- 17.x
- 18.x
- 19.x
- 20.x
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
Expand Down
151 changes: 86 additions & 65 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,22 @@

For more information, check out the [official documentation](https://axiom.co/docs).

## Installation
## Introduction

This library allows you to send Web Vitals as well as structured logs from your Next.js application to Axiom.

### Using Vercel Integration
## Installation

Make sure you have the [Axiom Vercel integration](https://www.axiom.co/vercel) installed. Once it is done, perform the steps below:
> **Note**
> Using Next.js 12? Use version `0.*`, which will continue to get security patches. Here's the [README for `0.18.0`](https://github.com/axiomhq/next-axiom/tree/v0.18.0). If you're upgrading to Next.js 13, check out the [next-axiom upgrade guide](#upgrade-to-nextjs-13).
- In your Next.js project, run install `next-axiom` as follows:
In your Next.js project, install next-axiom:

```sh
npm install --save next-axiom
npm install --save next-axiom@1.0.0-rc.1
```

- In the `next.config.js` file, wrap your Next.js config in `withAxiom` as follows:
In the `next.config.ts` file, wrap your Next.js config in `withAxiom` as follows:

```js
const { withAxiom } = require('next-axiom');
Expand All @@ -39,73 +42,97 @@ module.exports = withAxiom({
});
```

### Using Any Other Platform
If you are using the [Vercel integration](https://www.axiom.co/vercel),
no further configuration is required.

Create an API token in [Axiom settings](https://cloud.axiom.co/settings/profile) and export it as `AXIOM_TOKEN`, as well as the Axiom dataset name as `AXIOM_DATASET`. Once it is done, perform the steps below:
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`.

- In your Next.js project, run install `next-axiom` as follows:
## Usage

```sh
npm install --save next-axiom
```
### Web Vitals

- In the `next.config.js` file, wrap your Next.js config in `withAxiom` as follows:
Go to `app/layout.tsx` and add the Web Vitals component:

```js
const { withAxiom } = require('next-axiom');
```tsx
import { AxiomWebVitals } from 'next-axiom';

module.exports = withAxiom({
// ... your existing config
});
export default function RootLayout() {
return (
<html>
...
<AxiomWebVitals />
<div>...</div>
</html>
);
}
```

## Usage

### Web Vitals
> **Note**: WebVitals are only sent from production deployments.
> **Warning**: Web-Vitals are not yet supported in Next.js 13 and above. Please use Next.js 12 or below. We [submitted a patch](https://github.com/vercel/next.js/pull/47319) and as soon as Next.js 13.2.5 is out, we'll add support here.
### Logs

Go to `pages/_app.js` or `pages/_app.ts` and add the following line to report web vitals:
Send logs to Axiom from different parts of your application. Each log function call takes a message and an optional fields object.

```js
export { reportWebVitals } from 'next-axiom';
```typescript
log.debug('Login attempt', { user: 'j_doe', status: 'success' }); // results in {"message": "Login attempt", "fields": {"user": "j_doe", "status": "success"}}
log.info('Payment completed', { userID: '123', amount: '25USD' });
log.warn('API rate limit exceeded', { endpoint: '/users/1', rateLimitRemaining: 0 });
log.error('System Error', { code: '500', message: 'Internal server error' });
```

> **Note**: WebVitals are only sent from production deployments.
#### Route Handlers

Wrapping your handlers in `withAxiom` will make `req.log` available and log
exceptions:
Wrapping your Route Handlers in `withAxiom` will add a logger to your
request and automatically log exceptions:

```ts
import { withAxiom, AxiomAPIRequest } from 'next-axiom';
```typescript
import { withAxiom, AxiomRequest } from 'next-axiom';

async function handler(req: AxiomAPIRequest, res: NextApiResponse) {
export const GET = withAxiom((req: AxiomRequest) => {
req.log.info('Login function called');

// You can create intermediate loggers
const log = req.log.with({ scope: 'user' });
log.info('User logged in', { userId: 42 });

res.status(200).text('hi');
}
return NextResponse.json({ hello: 'world' });
});
```

#### Client Components

For Client Components, you can add a logger to your component with `useLogger`:

```tsx
'use client';
import { useLogger } from 'next-axiom';

export default withAxiom(handler);
export default function ClientComponent() {
const log = useLogger();
log.debug('User logged in', { userId: 42 });
return <h1>Logged in</h1>;
}
```

Import and use `log` in the frontend like this:
#### Server Components

```js
import { log } from `next-axiom`;
For Server Components, create a logger and make sure to call flush before returning:

```tsx
import { Logger } from 'next-axiom';

export default async function ServerComponent() {
const log = new Logger();
log.info('User logged in', { userId: 42 });

// pages/index.js
function home() {
...
log.debug('User logged in', { userId: 42 })
...
// ...

await log.flush();
return <h1>Logged in</h1>;
}
```

### Log Levels
#### Log Levels

The log level defines the lowest level of logs sent to Axiom.
The default is debug, resulting in all logs being sent.
Expand All @@ -114,40 +141,34 @@ Available levels are (from lowest to highest): `debug`, `info`, `warn`, `error`
For example, if you don't want debug logs to be sent to Axiom:

```sh
export AXIOM_LOG_LEVEL=info
export NEXT_PUBLIC_AXIOM_LOG_LEVEL=info
```

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

```sh
export AXIOM_LOG_LEVEL=off
```
## Upgrade to Next.js 13

### getServerSideProps
next-axiom switched to support Next.js 13 with app directory support starting version 0.19.0. If you are upgrading from Next.js 12, you will need to make the following changes:

To be able to use next-axiom with `getServerSideProps` you need to wrap your function with `withAxiomGetServerSideProps`, becasue there is no
way at the moment to automatically detected if getServerSideProps is used.

```ts
import { withAxiomGetServerSideProps } from 'next-axiom'
export const getServerSideProps = withAxiomGetServerSideProps(async ({ req, log }) => {
log.info('Hello, world!');
return {
props: {
},
}
});
```
- Upgrade next-axiom to version 1.0.0 or higher
- Make sure that exported variables has `NEXT_PUBLIC_` prefix, e.g: `NEXT_PUBLIC_AXIOM_TOKEN`
- Use `useLogger` hook in client components instead of `log` prop
- For server side components, you will need to create an instance of `Logger` and flush the logs before component returns.
- For web-vitals, remove `reportWebVitals()` and instead add the `AxiomWebVitals` component to your layout.

## FAQ

### How can I send logs from Vercel preview deployments?

The Axiom Vercel integration sets up an environment variable called `NEXT_PUBLIC_AXIOM_INGEST_ENDPOINT`, which by default is only enabled for the production environment. To send logs from preview deployments, go to your site settings in Vercel and enable preview deployments for that environment variable.

### How can I extend the logger?

You can use `log.with` to create an intermediate logger, for example:
```ts
const logger = log.with({ userId: 42 })
logger.info("Hi") // will ingest { ..., "message": "Hi", "fields" { "userId": 42 }}

```typescript
const logger = userLogger().with({ userId: 42 });
logger.info('Hi'); // will ingest { ..., "message": "Hi", "fields" { "userId": 42 }}
```

## License
Expand Down
13 changes: 7 additions & 6 deletions __tests__/genericConfig.vercel.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
process.env.AXIOM_INGEST_ENDPOINT = '';
process.env.AXIOM_URL = 'https://test.axiom.co';
process.env.AXIOM_DATASET = 'test';
process.env.NEXT_PUBLIC_AXIOM_INGEST_ENDPOINT = '';
process.env.NEXT_PUBLIC_AXIOM_URL = 'https://api.axiom.co';
process.env.NEXT_PUBLIC_AXIOM_DATASET = 'test';

import config from '../src/config';
import { config } from '../src/config';
import { EndpointType } from '../src/shared';
import { test, expect } from '@jest/globals';

test('reading axiom ingest endpoint', () => {
let url = config.getIngestURL(EndpointType.webVitals);
expect(url).toEqual('https://test.axiom.co/api/v1/datasets/test/ingest');
expect(url).toEqual('https://api.axiom.co/api/v1/datasets/test/ingest');

url = config.getIngestURL(EndpointType.logs);
expect(url).toEqual('https://test.axiom.co/api/v1/datasets/test/ingest');
expect(url).toEqual('https://api.axiom.co/api/v1/datasets/test/ingest');
});
47 changes: 34 additions & 13 deletions __tests__/log.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
/**
* @jest-environment jsdom
*/
// set axiom env vars before importing logger
process.env.AXIOM_INGEST_ENDPOINT = 'https://example.co/api/test';
process.env.NEXT_PUBLIC_AXIOM_INGEST_ENDPOINT = 'https://example.co/api/test';
import { log } from '../src/logger';
import { test, expect, jest } from '@jest/globals';

jest.useFakeTimers();

test('sending logs from browser', async () => {
global.fetch = jest.fn() as jest.Mock;
global.fetch = jest.fn(async () => {
const resp = new Response('', { status: 200 });
return Promise.resolve(resp);
}) as jest.Mock<typeof fetch>;

log.info('hello, world!');
expect(fetch).toHaveBeenCalledTimes(0);
Expand All @@ -24,15 +25,20 @@ test('sending logs from browser', async () => {
});

test('with', async () => {
global.fetch = jest.fn() as jest.Mock;
global.fetch = jest.fn(async () => {
const resp = new Response('', { status: 200 });
return Promise.resolve(resp);
}) as jest.Mock<typeof fetch>;

const logger = log.with({ foo: 'bar' });
logger.info('hello, world!', { bar: 'baz' });
expect(fetch).toHaveBeenCalledTimes(0);

jest.advanceTimersByTime(1000);
expect(fetch).toHaveBeenCalledTimes(1);
const payload = JSON.parse((fetch as jest.Mock).mock.calls[0][1].body);
const mockedFetch = fetch as jest.Mock<typeof fetch>;
const sentPayload = mockedFetch.mock.calls[0][1]?.body?.toString();
const payload = JSON.parse(sentPayload ? sentPayload : '{}');
expect(payload.length).toBe(1);
const fst = payload[0];
expect(fst.level).toBe('info');
Expand All @@ -43,7 +49,10 @@ test('with', async () => {
});

test('passing non-object', async () => {
global.fetch = jest.fn() as jest.Mock;
global.fetch = jest.fn(async () => {
const resp = new Response('', { status: 200 });
return Promise.resolve(resp);
}) as jest.Mock<typeof fetch>;

const logger = log.with({ foo: 'bar' });
const args = 'baz';
Expand All @@ -52,7 +61,9 @@ test('passing non-object', async () => {

jest.advanceTimersByTime(1000);
expect(fetch).toHaveBeenCalledTimes(1);
const payload = JSON.parse((fetch as jest.Mock).mock.calls[0][1].body);
const mockedFetch = fetch as jest.Mock<typeof fetch>;
const sentPayload = mockedFetch.mock.calls[0][1]?.body?.toString();
const payload = JSON.parse(sentPayload ? sentPayload : '{}');
expect(payload.length).toBe(1);
const fst = payload[0];
expect(fst.level).toBe('info');
Expand All @@ -62,7 +73,10 @@ test('passing non-object', async () => {
});

test('flushing child loggers', async () => {
global.fetch = jest.fn() as jest.Mock;
global.fetch = jest.fn(async () => {
const resp = new Response('', { status: 200 });
return Promise.resolve(resp);
}) as jest.Mock<typeof fetch>;

log.info('hello, world!');
const logger1 = log.with({ foo: 'bar' });
Expand All @@ -74,7 +88,9 @@ test('flushing child loggers', async () => {

expect(fetch).toHaveBeenCalledTimes(3);

const payload = JSON.parse((fetch as jest.Mock).mock.calls[2][1].body);
const mockedFetch = fetch as jest.Mock<typeof fetch>;
const sentPayload = mockedFetch.mock.calls[2][1]?.body?.toString();
const payload = JSON.parse(sentPayload ? sentPayload : '{}');
expect(Object.keys(payload[0].fields).length).toEqual(2);
expect(payload[0].fields.foo).toEqual('bar');
expect(payload[0].fields.bar).toEqual('foo');
Expand All @@ -84,12 +100,17 @@ test('flushing child loggers', async () => {
});

test('throwing exception', async () => {
global.fetch = jest.fn() as jest.Mock;
global.fetch = jest.fn(async () => {
const resp = new Response('', { status: 200 });
return Promise.resolve(resp);
}) as jest.Mock<typeof fetch>;
const err = new Error('test');
log.error('hello, world!', err);
await log.flush();
expect(fetch).toHaveBeenCalledTimes(1);
const payload = JSON.parse((fetch as jest.Mock).mock.calls[0][1].body);
const mockedFetch = fetch as jest.Mock<typeof fetch>;
const sentPayload = mockedFetch.mock.calls[0][1]?.body?.toString();
const payload = JSON.parse(sentPayload ? sentPayload : '{}');
expect(Object.keys(payload[0].fields).length).toEqual(3); // { name, message, stack }
expect(payload[0].fields.message).toEqual(err.message);
expect(payload[0].fields.name).toEqual(err.name);
Expand Down
Loading

1 comment on commit fa3a2c8

@vercel
Copy link

@vercel vercel bot commented on fa3a2c8 Jul 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

next-axiom-example – ./

next-axiom-example-git-main.axiom.dev
next-axiom-example.axiom.dev
next-axiom-example-lemon.vercel.app

Please sign in to comment.