diff --git a/apps/public/app/api/[...op]/route.ts b/apps/public/app/api/[...op]/route.ts
index 82945e54..8c9ad4a3 100644
--- a/apps/public/app/api/[...op]/route.ts
+++ b/apps/public/app/api/[...op]/route.ts
@@ -1,7 +1,5 @@
-import {
- createNextRouteHandler,
- createScriptHandler,
-} from '@openpanel/nextjs/server';
+import { createRouteHandler } from '@openpanel/nextjs/server';
-export const POST = createNextRouteHandler();
-export const GET = createScriptHandler();
+const routeHandler = createRouteHandler();
+export const GET = routeHandler;
+export const POST = routeHandler;
diff --git a/apps/public/app/test/page.tsx b/apps/public/app/test/page.tsx
new file mode 100644
index 00000000..36330dd5
--- /dev/null
+++ b/apps/public/app/test/page.tsx
@@ -0,0 +1,23 @@
+'use client';
+
+import { Button } from '@/components/ui/button';
+import { useOpenPanel } from '@openpanel/nextjs';
+
+export default function TestPage() {
+ const op = useOpenPanel();
+ return (
+
+
Test Page
+
+
+
+
+ );
+}
diff --git a/apps/public/content/docs/(tracking)/sdks/nextjs.mdx b/apps/public/content/docs/(tracking)/sdks/nextjs.mdx
index 1f0ad37f..8afef4a5 100644
--- a/apps/public/content/docs/(tracking)/sdks/nextjs.mdx
+++ b/apps/public/content/docs/(tracking)/sdks/nextjs.mdx
@@ -273,13 +273,16 @@ export function GET() {
### Proxy events
-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.
+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`).
```typescript title="/app/api/[...op]/route.ts"
-import { createNextRouteHandler, createScriptHandler } from '@openpanel/nextjs/server';
+import { createRouteHandler } from '@openpanel/nextjs/server';
-export const POST = createNextRouteHandler();
-export const GET = createScriptHandler()
+const routeHandler = createRouteHandler();
+
+// Export the same handler for all HTTP methods - it routes internally based on pathname
+export const GET = routeHandler;
+export const POST = routeHandler;
```
Remember to change the `apiUrl` and `cdnUrl` in the `OpenPanelComponent` to your own server.
diff --git a/apps/public/package.json b/apps/public/package.json
index 9eeb79b2..231f4690 100644
--- a/apps/public/package.json
+++ b/apps/public/package.json
@@ -14,7 +14,7 @@
"@hyperdx/node-opentelemetry": "^0.8.1",
"@number-flow/react": "0.3.5",
"@openpanel/common": "workspace:*",
- "@openpanel/nextjs": "^1.0.17",
+ "@openpanel/nextjs": "^1.1.0",
"@openpanel/payments": "workspace:^",
"@openpanel/sdk-info": "workspace:^",
"@openstatus/react": "0.0.3",
diff --git a/packages/sdks/nextjs/createNextRouteHandler.ts b/packages/sdks/nextjs/createNextRouteHandler.ts
index 35dccdcc..41e91ab3 100644
--- a/packages/sdks/nextjs/createNextRouteHandler.ts
+++ b/packages/sdks/nextjs/createNextRouteHandler.ts
@@ -3,80 +3,141 @@ import { createHash } from 'node:crypto';
// with esm and nextjs (when using pages dir)
import { NextResponse } from 'next/server.js';
-type CreateNextRouteHandlerOptions = {
+type RouteHandlerOptions = {
apiUrl?: string;
};
-export function createNextRouteHandler(
- options?: CreateNextRouteHandlerOptions,
-) {
- return async function POST(req: Request) {
- const apiUrl = options?.apiUrl ?? 'https://api.openpanel.dev';
- const headers = new Headers();
+const DEFAULT_API_URL = 'https://api.openpanel.dev';
+const SCRIPT_URL = 'https://openpanel.dev';
+const SCRIPT_PATH = '/op1.js';
- const ip =
- req.headers.get('cf-connecting-ip') ??
- req.headers.get('x-forwarded-for')?.split(',')[0] ??
- req.headers.get('x-vercel-forwarded-for');
- headers.set('Content-Type', 'application/json');
- headers.set(
- 'openpanel-client-id',
- req.headers.get('openpanel-client-id') ?? '',
- );
- headers.set('origin', req.headers.get('origin') ?? '');
- headers.set('User-Agent', req.headers.get('user-agent') ?? '');
- if (ip) {
- headers.set('openpanel-client-ip', ip);
- }
+function getClientHeaders(req: Request): Headers {
+ const headers = new Headers();
+ const ip =
+ req.headers.get('cf-connecting-ip') ??
+ req.headers.get('x-forwarded-for')?.split(',')[0] ??
+ req.headers.get('x-vercel-forwarded-for');
- try {
- const res = await fetch(`${apiUrl}/track`, {
- method: 'POST',
- headers,
- body: JSON.stringify(await req.json()),
- });
- return NextResponse.json(await res.text(), { status: res.status });
- } catch (e) {
- return NextResponse.json(e);
- }
- };
+ headers.set('Content-Type', 'application/json');
+ headers.set(
+ 'openpanel-client-id',
+ req.headers.get('openpanel-client-id') ?? '',
+ );
+
+ // Construct origin: browsers send Origin header for POST requests and cross-origin requests,
+ // but not for same-origin GET requests. Fallback to constructing from request URL.
+ const origin =
+ req.headers.get('origin') ??
+ (() => {
+ const url = new URL(req.url);
+ return `${url.protocol}//${url.host}`;
+ })();
+ headers.set('origin', origin);
+
+ headers.set('User-Agent', req.headers.get('user-agent') ?? '');
+ if (ip) {
+ headers.set('openpanel-client-ip', ip);
+ }
+
+ return headers;
}
-export function createScriptHandler() {
- return async function GET(req: Request) {
- const url = new URL(req.url);
- const query = url.searchParams.toString();
+async function handleApiRoute(
+ req: Request,
+ apiUrl: string,
+ apiPath: string,
+): Promise {
+ const headers = getClientHeaders(req);
- if (!url.pathname.endsWith('op1.js')) {
+ try {
+ const res = await fetch(`${apiUrl}${apiPath}`, {
+ method: req.method,
+ headers,
+ body:
+ req.method === 'POST' ? JSON.stringify(await req.json()) : undefined,
+ });
+
+ if (res.headers.get('content-type')?.includes('application/json')) {
+ return NextResponse.json(await res.json(), { status: res.status });
+ }
+ return NextResponse.json(await res.text(), { status: res.status });
+ } catch (e) {
+ return NextResponse.json(
+ {
+ error: 'Failed to proxy request',
+ message: e instanceof Error ? e.message : String(e),
+ },
+ { status: 500 },
+ );
+ }
+}
+
+async function handleScriptProxyRoute(req: Request): Promise {
+ const url = new URL(req.url);
+ const pathname = url.pathname;
+
+ if (!pathname.endsWith(SCRIPT_PATH)) {
+ return NextResponse.json({ error: 'Not found' }, { status: 404 });
+ }
+
+ let scriptUrl = `${SCRIPT_URL}${SCRIPT_PATH}`;
+ if (url.searchParams.size > 0) {
+ scriptUrl += `?${url.searchParams.toString()}`;
+ }
+
+ try {
+ const res = await fetch(scriptUrl, {
+ // @ts-ignore
+ next: { revalidate: 86400 },
+ });
+ const text = await res.text();
+ const etag = `"${createHash('md5')
+ .update(scriptUrl + text)
+ .digest('hex')}"`;
+
+ return new NextResponse(text, {
+ headers: {
+ 'Content-Type': 'text/javascript',
+ 'Cache-Control': 'public, max-age=86400, stale-while-revalidate=86400',
+ ETag: etag,
+ },
+ });
+ } catch (e) {
+ return NextResponse.json(
+ {
+ error: 'Failed to fetch script',
+ message: e instanceof Error ? e.message : String(e),
+ },
+ { status: 500 },
+ );
+ }
+}
+
+function createRouteHandler(options?: RouteHandlerOptions) {
+ const apiUrl = options?.apiUrl ?? DEFAULT_API_URL;
+
+ return async function handler(req: Request): Promise {
+ const url = new URL(req.url);
+ const pathname = url.pathname;
+ const method = req.method;
+
+ // Handle script proxy: GET /op1.js
+ if (method === 'GET' && pathname.endsWith(SCRIPT_PATH)) {
+ return handleScriptProxyRoute(req);
+ }
+
+ const apiPathMatch = pathname.indexOf('/track');
+ if (apiPathMatch === -1) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
- const scriptUrl = 'https://openpanel.dev/op1.js';
- try {
- const res = await fetch(scriptUrl, {
- // @ts-ignore
- next: { revalidate: 86400 },
- });
- const text = await res.text();
- const etag = `"${createHash('md5')
- .update(text + query)
- .digest('hex')}"`;
- return new NextResponse(text, {
- headers: {
- 'Content-Type': 'text/javascript',
- 'Cache-Control':
- 'public, max-age=86400, stale-while-revalidate=86400',
- ETag: etag,
- },
- });
- } catch (e) {
- return NextResponse.json(
- {
- error: 'Failed to fetch script',
- message: e instanceof Error ? e.message : String(e),
- },
- { status: 500 },
- );
- }
+ const apiPath = pathname.substring(apiPathMatch);
+ return handleApiRoute(req, apiUrl, apiPath);
};
}
+
+export { createRouteHandler };
+
+// const routeHandler = createRouteHandler();
+// export const GET = routeHandler;
+// export const POST = routeHandler;
diff --git a/packages/sdks/nextjs/index.tsx b/packages/sdks/nextjs/index.tsx
index 123ac958..56e22cce 100644
--- a/packages/sdks/nextjs/index.tsx
+++ b/packages/sdks/nextjs/index.tsx
@@ -68,9 +68,17 @@ export function OpenPanelComponent({
value: globalProperties,
});
}
+
+ const appendVersion = (url: string) => {
+ if (url.endsWith('.js')) {
+ return `${url}?v=${process.env.NEXTJS_VERSION!}`;
+ }
+ return url;
+ };
+
return (
<>
-
+