feat: improve nextjs proxying mode
This commit is contained in:
@@ -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;
|
||||
|
||||
23
apps/public/app/test/page.tsx
Normal file
23
apps/public/app/test/page.tsx
Normal file
@@ -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 (
|
||||
<div>
|
||||
<h1>Test Page</h1>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const deviceId = await op.fetchDeviceId();
|
||||
alert(`Device ID: ${deviceId}`);
|
||||
}}
|
||||
>
|
||||
Fetch device id
|
||||
</Button>
|
||||
<Button onClick={() => op.track('hello')}>Hello</Button>
|
||||
<Button onClick={() => op.revenue(100)}>Revenue</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<NextResponse> {
|
||||
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<NextResponse> {
|
||||
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<NextResponse> {
|
||||
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;
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Script src={cdnUrl ?? CDN_URL} async defer />
|
||||
<Script src={appendVersion(cdnUrl || CDN_URL)} async defer />
|
||||
<Script
|
||||
strategy="beforeInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openpanel/nextjs",
|
||||
"version": "1.0.20-local",
|
||||
"version": "1.1.0-local",
|
||||
"module": "index.ts",
|
||||
"scripts": {
|
||||
"build": "rm -rf dist && tsup",
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
export {
|
||||
createNextRouteHandler,
|
||||
createScriptHandler,
|
||||
} from './createNextRouteHandler';
|
||||
export { createRouteHandler } from './createNextRouteHandler';
|
||||
|
||||
26
pnpm-lock.yaml
generated
26
pnpm-lock.yaml
generated
@@ -275,8 +275,8 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/common
|
||||
'@openpanel/nextjs':
|
||||
specifier: ^1.0.17
|
||||
version: 1.0.17(next@15.0.3(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0(next@15.0.3(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
'@openpanel/payments':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/payments
|
||||
@@ -4796,8 +4796,8 @@ packages:
|
||||
resolution: {integrity: sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
|
||||
'@openpanel/nextjs@1.0.17':
|
||||
resolution: {integrity: sha512-vH1PHlO43WJq2wOpQ36kxvWqsht6S26HHC/NyjA9uPVGAptYlXqVJi4fx5MI79azTaGMZzuxmW2yXAJhy9gjhQ==}
|
||||
'@openpanel/nextjs@1.1.0':
|
||||
resolution: {integrity: sha512-43UM4/Vau3SxwawsgA9NRKLSFNZOvSDnJA5VZS4fnXdzc6xZ64Id82q9onrNiUItMlaV9NqVJaKGPoYfbRP9lg==}
|
||||
peerDependencies:
|
||||
next: ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
@@ -4806,14 +4806,14 @@ packages:
|
||||
'@openpanel/sdk@1.0.0':
|
||||
resolution: {integrity: sha512-FNmmfjdXoC/VHEjA+WkrQ4lyM5lxEmV7xDd57uj4E+lIS0sU3DLG2mV/dpS8AscnZbUvuMn3kPhiLCqYzuv/gg==}
|
||||
|
||||
'@openpanel/sdk@1.0.1':
|
||||
resolution: {integrity: sha512-9YQlrEnmqyr5u+y+MRE/pHYnrtV/oInPJZclVqVDiiQyEL4GmpaU/ZAbJPNVln7ZPw4i5Voj7vrlVY3rlbWfyw==}
|
||||
'@openpanel/sdk@1.0.2':
|
||||
resolution: {integrity: sha512-WvVWCBcJvJhM5MYKO5Hxjo4G/E0tnK5XK2UC+hKDHtoF+iKvtUWa5bz18pFAPQprq0u/Ev2YqirPsrMQJy5g2g==}
|
||||
|
||||
'@openpanel/web@1.0.1':
|
||||
resolution: {integrity: sha512-cVZ7Kr9SicczJ/RDIfEtZs8+1iGDzwkabVA/j3NqSl8VSucsC8m1+LVbjmCDzCJNnK4yVn6tEcc9PJRi2rtllw==}
|
||||
|
||||
'@openpanel/web@1.0.4':
|
||||
resolution: {integrity: sha512-wEmVXAk6K5hRO/2KJc7GSrt6V8YhzmzntyLkEPshysnFMsWLNFDYUdu11sNy+Wn0cEb4HKDv/Nn34wNLJX0ydw==}
|
||||
'@openpanel/web@1.0.5':
|
||||
resolution: {integrity: sha512-n/A9fKiHWcDTH2N6N8MM214ET7aoNJjgpLux0GRW+CD0KDEwI8UosQvvz3UOGHZ3jWqMMsUNdU2B7eYk2W87mg==}
|
||||
|
||||
'@openstatus/react@0.0.3':
|
||||
resolution: {integrity: sha512-uDiegz7e3H67pG8lTT+op+6w5keTT7XpcENrREaqlWl5j53TYyO8nheOG1PeNw2/Qgd5KaGeRJJFn1crhTUSYw==}
|
||||
@@ -19884,24 +19884,24 @@ snapshots:
|
||||
|
||||
'@oozcitak/util@8.3.8': {}
|
||||
|
||||
'@openpanel/nextjs@1.0.17(next@15.0.3(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
|
||||
'@openpanel/nextjs@1.1.0(next@15.0.3(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
|
||||
dependencies:
|
||||
'@openpanel/web': 1.0.4
|
||||
'@openpanel/web': 1.0.5
|
||||
next: 15.0.3(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
react: 19.1.1
|
||||
react-dom: 19.1.1(react@19.1.1)
|
||||
|
||||
'@openpanel/sdk@1.0.0': {}
|
||||
|
||||
'@openpanel/sdk@1.0.1': {}
|
||||
'@openpanel/sdk@1.0.2': {}
|
||||
|
||||
'@openpanel/web@1.0.1':
|
||||
dependencies:
|
||||
'@openpanel/sdk': 1.0.0
|
||||
|
||||
'@openpanel/web@1.0.4':
|
||||
'@openpanel/web@1.0.5':
|
||||
dependencies:
|
||||
'@openpanel/sdk': 1.0.1
|
||||
'@openpanel/sdk': 1.0.2
|
||||
|
||||
'@openstatus/react@0.0.3(react@19.1.1)':
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user