Skip to content

Commit b18e60a

Browse files
committed
test
1 parent c1801ad commit b18e60a

File tree

7 files changed

+157
-6
lines changed

7 files changed

+157
-6
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { createHash } from 'node:crypto';
2+
import { getClientIpFromHeaders } from '@openpanel/common/server/get-client-ip';
3+
// adding .js next/script import fixes an issues
4+
// with esm and nextjs (when using pages dir)
5+
import { NextResponse } from 'next/server.js';
6+
7+
type CreateNextRouteHandlerOptions = {
8+
apiUrl?: string;
9+
};
10+
11+
function createNextRouteHandler(options: CreateNextRouteHandlerOptions) {
12+
return async function POST(req: Request) {
13+
const apiUrl = options.apiUrl ?? 'https://api.openpanel.dev';
14+
const headers = new Headers(req.headers);
15+
const clientIp = getClientIpFromHeaders(headers);
16+
console.log('debug', {
17+
clientIp,
18+
userAgent: req.headers.get('user-agent'),
19+
});
20+
try {
21+
const res = await fetch(`${apiUrl}/track`, {
22+
method: 'POST',
23+
headers,
24+
body: JSON.stringify(await req.json()),
25+
});
26+
return NextResponse.json(await res.text(), { status: res.status });
27+
} catch (e) {
28+
return NextResponse.json(e);
29+
}
30+
};
31+
}
32+
33+
function createScriptHandler() {
34+
return async function GET(req: Request) {
35+
if (!req.url.endsWith('op1.js')) {
36+
return NextResponse.json({ error: 'Not found' }, { status: 404 });
37+
}
38+
39+
const scriptUrl = 'https://openpanel.dev/op1.js';
40+
try {
41+
const res = await fetch(scriptUrl, {
42+
next: { revalidate: 86400 },
43+
});
44+
const text = await res.text();
45+
const etag = `"${createHash('md5').update(text).digest('hex')}"`;
46+
return new NextResponse(text, {
47+
headers: {
48+
'Content-Type': 'text/javascript',
49+
'Cache-Control':
50+
'public, max-age=86400, stale-while-revalidate=86400',
51+
ETag: etag,
52+
},
53+
});
54+
} catch (e) {
55+
return NextResponse.json(
56+
{
57+
error: 'Failed to fetch script',
58+
message: e instanceof Error ? e.message : String(e),
59+
},
60+
{ status: 500 },
61+
);
62+
}
63+
};
64+
}
65+
66+
export const POST = createNextRouteHandler({});
67+
export const GET = createScriptHandler();

apps/public/app/layout.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,9 @@ export default async function Layout({ children }: { children: ReactNode }) {
6161
<RootProvider>
6262
<TooltipProvider>{children}</TooltipProvider>
6363
</RootProvider>
64-
<Script
65-
defer
66-
src="http://localhost:3000/script.js"
67-
data-website-id="44d65df1-e9cb-4c2c-917d-4bf1c7850948"
68-
/>
6964
<OpenPanelComponent
65+
apiUrl="/api/op"
66+
cdnUrl="/api/op/op1.js"
7067
clientId="301c6dc1-424c-4bc3-9886-a8beab09b615"
7168
trackAttributes
7269
trackScreenViews

apps/public/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"dependencies": {
1414
"@hyperdx/node-opentelemetry": "^0.8.1",
1515
"@number-flow/react": "0.3.5",
16+
"@openpanel/common": "workspace:*",
1617
"@openpanel/nextjs": "^1.0.5",
1718
"@openpanel/payments": "workspace:^",
1819
"@openpanel/sdk-info": "workspace:^",

packages/common/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ export * from './src/url';
99
export * from './src/id';
1010
export * from './src/get-previous-metric';
1111
export * from './src/group-by-labels';
12+
export * from './src/get-client-ip';

packages/common/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"main": "index.ts",
66
"exports": {
77
".": "./index.ts",
8-
"./server": "./server/index.ts"
8+
"./server": "./server/index.ts",
9+
"./server/get-client-ip": "./server/get-client-ip.ts"
910
},
1011
"scripts": {
1112
"test": "vitest",
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* Get client IP from headers
3+
*
4+
* Can be configured via IP_HEADER_ORDER env variable
5+
* Example: IP_HEADER_ORDER="cf-connecting-ip,x-real-ip,x-forwarded-for"
6+
*/
7+
8+
const DEFAULT_HEADER_ORDER = [
9+
'cf-connecting-ip',
10+
'true-client-ip',
11+
'x-real-ip',
12+
'x-client-ip',
13+
'fastly-client-ip',
14+
'x-cluster-client-ip',
15+
'x-appengine-user-ip',
16+
'do-connecting-ip',
17+
'x-nf-client-connection-ip',
18+
'x-forwarded-for',
19+
'x-forwarded',
20+
'forwarded',
21+
];
22+
23+
function getHeaderOrder(): string[] {
24+
if (typeof process !== 'undefined' && process.env?.IP_HEADER_ORDER) {
25+
return process.env.IP_HEADER_ORDER.split(',').map((h) => h.trim());
26+
}
27+
return DEFAULT_HEADER_ORDER;
28+
}
29+
30+
function isValidIp(ip: string): boolean {
31+
// Basic IP validation
32+
const ipv4 = /^(\d{1,3}\.){3}\d{1,3}$/;
33+
const ipv6 = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/;
34+
return ipv4.test(ip) || ipv6.test(ip);
35+
}
36+
37+
export function getClientIpFromHeaders(
38+
headers: Record<string, string | string[] | undefined> | Headers,
39+
): string | null {
40+
const headerOrder = getHeaderOrder();
41+
42+
for (const headerName of headerOrder) {
43+
let value: string | null = null;
44+
45+
// Get header value
46+
if (headers instanceof Headers) {
47+
value = headers.get(headerName);
48+
} else {
49+
const headerValue = headers[headerName];
50+
if (Array.isArray(headerValue)) {
51+
value = headerValue[0] || null;
52+
} else {
53+
value = headerValue || null;
54+
}
55+
}
56+
57+
if (!value) continue;
58+
59+
// Handle x-forwarded-for (comma separated)
60+
if (headerName === 'x-forwarded-for') {
61+
const firstIp = value.split(',')[0]?.trim();
62+
if (firstIp && isValidIp(firstIp)) {
63+
return firstIp;
64+
}
65+
}
66+
// Handle forwarded header (RFC 7239)
67+
else if (headerName === 'forwarded') {
68+
const match = value.match(/for=(?:"?\[?([^\]"]+)\]?"?)/i);
69+
const ip = match?.[1];
70+
if (ip && isValidIp(ip)) {
71+
return ip;
72+
}
73+
}
74+
// Regular headers
75+
else if (isValidIp(value)) {
76+
return value;
77+
}
78+
}
79+
80+
return null;
81+
}

pnpm-lock.yaml

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

0 commit comments

Comments
 (0)