Skip to content

Commit 6da8267

Browse files
committed
feat: improve nextjs proxying mode
1 parent 86903b1 commit 6da8267

File tree

9 files changed

+183
-93
lines changed

9 files changed

+183
-93
lines changed
Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import {
2-
createNextRouteHandler,
3-
createScriptHandler,
4-
} from '@openpanel/nextjs/server';
1+
import { createRouteHandler } from '@openpanel/nextjs/server';
52

6-
export const POST = createNextRouteHandler();
7-
export const GET = createScriptHandler();
3+
const routeHandler = createRouteHandler();
4+
export const GET = routeHandler;
5+
export const POST = routeHandler;

apps/public/app/test/page.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use client';
2+
3+
import { Button } from '@/components/ui/button';
4+
import { useOpenPanel } from '@openpanel/nextjs';
5+
6+
export default function TestPage() {
7+
const op = useOpenPanel();
8+
return (
9+
<div>
10+
<h1>Test Page</h1>
11+
<Button
12+
onClick={async () => {
13+
const deviceId = await op.fetchDeviceId();
14+
alert(`Device ID: ${deviceId}`);
15+
}}
16+
>
17+
Fetch device id
18+
</Button>
19+
<Button onClick={() => op.track('hello')}>Hello</Button>
20+
<Button onClick={() => op.revenue(100)}>Revenue</Button>
21+
</div>
22+
);
23+
}

apps/public/content/docs/(tracking)/sdks/nextjs.mdx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -273,13 +273,16 @@ export function GET() {
273273

274274
### Proxy events
275275

276-
With `createNextRouteHandler` you can proxy your events through your server, this will ensure all events are tracked since there is a lot of adblockers that block requests to third party domains. You'll also need to either host our tracking script or you can use `createScriptHandler` function which proxies this as well.
276+
With `createRouteHandler` you can proxy your events through your server, this will ensure all events are tracked since there is a lot of adblockers that block requests to third party domains. The handler automatically routes requests based on the path, supporting both API endpoints (like `/track` and `/track/device-id`) and the tracking script (`/op1.js`).
277277

278278
```typescript title="/app/api/[...op]/route.ts"
279-
import { createNextRouteHandler, createScriptHandler } from '@openpanel/nextjs/server';
279+
import { createRouteHandler } from '@openpanel/nextjs/server';
280280

281-
export const POST = createNextRouteHandler();
282-
export const GET = createScriptHandler()
281+
const routeHandler = createRouteHandler();
282+
283+
// Export the same handler for all HTTP methods - it routes internally based on pathname
284+
export const GET = routeHandler;
285+
export const POST = routeHandler;
283286
```
284287

285288
Remember to change the `apiUrl` and `cdnUrl` in the `OpenPanelComponent` to your own server.

apps/public/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"@hyperdx/node-opentelemetry": "^0.8.1",
1515
"@number-flow/react": "0.3.5",
1616
"@openpanel/common": "workspace:*",
17-
"@openpanel/nextjs": "^1.0.17",
17+
"@openpanel/nextjs": "^1.1.0",
1818
"@openpanel/payments": "workspace:^",
1919
"@openpanel/sdk-info": "workspace:^",
2020
"@openstatus/react": "0.0.3",

packages/sdks/nextjs/createNextRouteHandler.ts

Lines changed: 124 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -3,80 +3,141 @@ import { createHash } from 'node:crypto';
33
// with esm and nextjs (when using pages dir)
44
import { NextResponse } from 'next/server.js';
55

6-
type CreateNextRouteHandlerOptions = {
6+
type RouteHandlerOptions = {
77
apiUrl?: string;
88
};
99

10-
export function createNextRouteHandler(
11-
options?: CreateNextRouteHandlerOptions,
12-
) {
13-
return async function POST(req: Request) {
14-
const apiUrl = options?.apiUrl ?? 'https://api.openpanel.dev';
15-
const headers = new Headers();
16-
17-
const ip =
18-
req.headers.get('cf-connecting-ip') ??
19-
req.headers.get('x-forwarded-for')?.split(',')[0] ??
20-
req.headers.get('x-vercel-forwarded-for');
21-
headers.set('Content-Type', 'application/json');
22-
headers.set(
23-
'openpanel-client-id',
24-
req.headers.get('openpanel-client-id') ?? '',
25-
);
26-
headers.set('origin', req.headers.get('origin') ?? '');
27-
headers.set('User-Agent', req.headers.get('user-agent') ?? '');
28-
if (ip) {
29-
headers.set('openpanel-client-ip', ip);
30-
}
10+
const DEFAULT_API_URL = 'https://api.openpanel.dev';
11+
const SCRIPT_URL = 'https://openpanel.dev';
12+
const SCRIPT_PATH = '/op1.js';
13+
14+
function getClientHeaders(req: Request): Headers {
15+
const headers = new Headers();
16+
const ip =
17+
req.headers.get('cf-connecting-ip') ??
18+
req.headers.get('x-forwarded-for')?.split(',')[0] ??
19+
req.headers.get('x-vercel-forwarded-for');
20+
21+
headers.set('Content-Type', 'application/json');
22+
headers.set(
23+
'openpanel-client-id',
24+
req.headers.get('openpanel-client-id') ?? '',
25+
);
3126

32-
try {
33-
const res = await fetch(`${apiUrl}/track`, {
34-
method: 'POST',
35-
headers,
36-
body: JSON.stringify(await req.json()),
37-
});
38-
return NextResponse.json(await res.text(), { status: res.status });
39-
} catch (e) {
40-
return NextResponse.json(e);
27+
// Construct origin: browsers send Origin header for POST requests and cross-origin requests,
28+
// but not for same-origin GET requests. Fallback to constructing from request URL.
29+
const origin =
30+
req.headers.get('origin') ??
31+
(() => {
32+
const url = new URL(req.url);
33+
return `${url.protocol}//${url.host}`;
34+
})();
35+
headers.set('origin', origin);
36+
37+
headers.set('User-Agent', req.headers.get('user-agent') ?? '');
38+
if (ip) {
39+
headers.set('openpanel-client-ip', ip);
40+
}
41+
42+
return headers;
43+
}
44+
45+
async function handleApiRoute(
46+
req: Request,
47+
apiUrl: string,
48+
apiPath: string,
49+
): Promise<NextResponse> {
50+
const headers = getClientHeaders(req);
51+
52+
try {
53+
const res = await fetch(`${apiUrl}${apiPath}`, {
54+
method: req.method,
55+
headers,
56+
body:
57+
req.method === 'POST' ? JSON.stringify(await req.json()) : undefined,
58+
});
59+
60+
if (res.headers.get('content-type')?.includes('application/json')) {
61+
return NextResponse.json(await res.json(), { status: res.status });
4162
}
42-
};
63+
return NextResponse.json(await res.text(), { status: res.status });
64+
} catch (e) {
65+
return NextResponse.json(
66+
{
67+
error: 'Failed to proxy request',
68+
message: e instanceof Error ? e.message : String(e),
69+
},
70+
{ status: 500 },
71+
);
72+
}
4373
}
4474

45-
export function createScriptHandler() {
46-
return async function GET(req: Request) {
75+
async function handleScriptProxyRoute(req: Request): Promise<NextResponse> {
76+
const url = new URL(req.url);
77+
const pathname = url.pathname;
78+
79+
if (!pathname.endsWith(SCRIPT_PATH)) {
80+
return NextResponse.json({ error: 'Not found' }, { status: 404 });
81+
}
82+
83+
let scriptUrl = `${SCRIPT_URL}${SCRIPT_PATH}`;
84+
if (url.searchParams.size > 0) {
85+
scriptUrl += `?${url.searchParams.toString()}`;
86+
}
87+
88+
try {
89+
const res = await fetch(scriptUrl, {
90+
// @ts-ignore
91+
next: { revalidate: 86400 },
92+
});
93+
const text = await res.text();
94+
const etag = `"${createHash('md5')
95+
.update(scriptUrl + text)
96+
.digest('hex')}"`;
97+
98+
return new NextResponse(text, {
99+
headers: {
100+
'Content-Type': 'text/javascript',
101+
'Cache-Control': 'public, max-age=86400, stale-while-revalidate=86400',
102+
ETag: etag,
103+
},
104+
});
105+
} catch (e) {
106+
return NextResponse.json(
107+
{
108+
error: 'Failed to fetch script',
109+
message: e instanceof Error ? e.message : String(e),
110+
},
111+
{ status: 500 },
112+
);
113+
}
114+
}
115+
116+
function createRouteHandler(options?: RouteHandlerOptions) {
117+
const apiUrl = options?.apiUrl ?? DEFAULT_API_URL;
118+
119+
return async function handler(req: Request): Promise<NextResponse> {
47120
const url = new URL(req.url);
48-
const query = url.searchParams.toString();
121+
const pathname = url.pathname;
122+
const method = req.method;
49123

50-
if (!url.pathname.endsWith('op1.js')) {
51-
return NextResponse.json({ error: 'Not found' }, { status: 404 });
124+
// Handle script proxy: GET /op1.js
125+
if (method === 'GET' && pathname.endsWith(SCRIPT_PATH)) {
126+
return handleScriptProxyRoute(req);
52127
}
53128

54-
const scriptUrl = 'https://openpanel.dev/op1.js';
55-
try {
56-
const res = await fetch(scriptUrl, {
57-
// @ts-ignore
58-
next: { revalidate: 86400 },
59-
});
60-
const text = await res.text();
61-
const etag = `"${createHash('md5')
62-
.update(text + query)
63-
.digest('hex')}"`;
64-
return new NextResponse(text, {
65-
headers: {
66-
'Content-Type': 'text/javascript',
67-
'Cache-Control':
68-
'public, max-age=86400, stale-while-revalidate=86400',
69-
ETag: etag,
70-
},
71-
});
72-
} catch (e) {
73-
return NextResponse.json(
74-
{
75-
error: 'Failed to fetch script',
76-
message: e instanceof Error ? e.message : String(e),
77-
},
78-
{ status: 500 },
79-
);
129+
const apiPathMatch = pathname.indexOf('/track');
130+
if (apiPathMatch === -1) {
131+
return NextResponse.json({ error: 'Not found' }, { status: 404 });
80132
}
133+
134+
const apiPath = pathname.substring(apiPathMatch);
135+
return handleApiRoute(req, apiUrl, apiPath);
81136
};
82137
}
138+
139+
export { createRouteHandler };
140+
141+
// const routeHandler = createRouteHandler();
142+
// export const GET = routeHandler;
143+
// export const POST = routeHandler;

packages/sdks/nextjs/index.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,17 @@ export function OpenPanelComponent({
6868
value: globalProperties,
6969
});
7070
}
71+
72+
const appendVersion = (url: string) => {
73+
if (url.endsWith('.js')) {
74+
return `${url}?v=${process.env.NEXTJS_VERSION!}`;
75+
}
76+
return url;
77+
};
78+
7179
return (
7280
<>
73-
<Script src={cdnUrl ?? CDN_URL} async defer />
81+
<Script src={appendVersion(cdnUrl || CDN_URL)} async defer />
7482
<Script
7583
strategy="beforeInteractive"
7684
dangerouslySetInnerHTML={{

packages/sdks/nextjs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@openpanel/nextjs",
3-
"version": "1.0.20-local",
3+
"version": "1.1.0-local",
44
"module": "index.ts",
55
"scripts": {
66
"build": "rm -rf dist && tsup",

packages/sdks/nextjs/server.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1 @@
1-
export {
2-
createNextRouteHandler,
3-
createScriptHandler,
4-
} from './createNextRouteHandler';
1+
export { createRouteHandler } from './createNextRouteHandler';

pnpm-lock.yaml

Lines changed: 13 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)