This commit is contained in:
Carl-Gerhard Lindesvärd
2024-08-01 22:01:46 +02:00
committed by Carl-Gerhard Lindesvärd
parent 15e997129a
commit 2226cb463d
10 changed files with 73 additions and 83 deletions

View File

@@ -22,6 +22,7 @@ import importRouter from './routes/import.router';
import liveRouter from './routes/live.router'; import liveRouter from './routes/live.router';
import miscRouter from './routes/misc.router'; import miscRouter from './routes/misc.router';
import profileRouter from './routes/profile.router'; import profileRouter from './routes/profile.router';
import trackRouter from './routes/track.router';
import webhookRouter from './routes/webhook.router'; import webhookRouter from './routes/webhook.router';
import { logger, logInfo } from './utils/logger'; import { logger, logInfo } from './utils/logger';
@@ -135,6 +136,7 @@ const startServer = async () => {
fastify.register(exportRouter, { prefix: '/export' }); fastify.register(exportRouter, { prefix: '/export' });
fastify.register(webhookRouter, { prefix: '/webhook' }); fastify.register(webhookRouter, { prefix: '/webhook' });
fastify.register(importRouter, { prefix: '/import' }); fastify.register(importRouter, { prefix: '/import' });
fastify.register(trackRouter, { prefix: '/track' });
fastify.setErrorHandler((error) => { fastify.setErrorHandler((error) => {
logger.error(error, 'Error in request'); logger.error(error, 'Error in request');
}); });

View File

@@ -1,13 +1,13 @@
import { isBot } from '@/bots'; import { isBot } from '@/bots';
import type { TrackHandlerPayload } from '@/controllers/track.controller';
import { handler } from '@/controllers/track.controller'; import { handler } from '@/controllers/track.controller';
import { SdkAuthError, validateSdkRequest } from '@/utils/auth'; import { SdkAuthError, validateSdkRequest } from '@/utils/auth';
import { logger } from '@/utils/logger'; import { logger } from '@/utils/logger';
import type { FastifyPluginCallback, FastifyRequest } from 'fastify'; import type { FastifyPluginCallback, FastifyRequest } from 'fastify';
import { createBotEvent } from '@openpanel/db'; import { createBotEvent } from '@openpanel/db';
import type { TrackHandlerPayload } from '@openpanel/sdk';
const eventRouter: FastifyPluginCallback = (fastify, opts, done) => { const trackRouter: FastifyPluginCallback = (fastify, opts, done) => {
fastify.addHook( fastify.addHook(
'preHandler', 'preHandler',
async ( async (
@@ -66,4 +66,4 @@ const eventRouter: FastifyPluginCallback = (fastify, opts, done) => {
done(); done();
}; };
export default eventRouter; export default trackRouter;

View File

@@ -0,0 +1,2 @@
"use strict";(()=>{var d=class{constructor(e){this.baseUrl=e.baseUrl,this.headers={"Content-Type":"application/json",...e.defaultHeaders},this.maxRetries=e.maxRetries??3,this.initialRetryDelay=e.initialRetryDelay??500}async resolveHeaders(){let e={};for(let[r,i]of Object.entries(this.headers)){let t=await i;t!==null&&(e[r]=t)}return e}addHeader(e,r){this.headers[e]=r}async post(e,r,i,t){try{let s=await fetch(e,{method:"POST",headers:await this.resolveHeaders(),body:JSON.stringify(r??{}),keepalive:!0,...i});if(s.status===401)return null;if(s.status!==200&&s.status!==202)throw new Error(`HTTP error! status: ${s.status}`);let n=await s.text();return n?JSON.parse(n):null}catch(s){if(t<this.maxRetries){let n=this.initialRetryDelay*Math.pow(2,t);return await new Promise(a=>setTimeout(a,n)),this.post(e,r,i,t+1)}return console.error("Max retries reached:",s),null}}async fetch(e,r,i={}){let t=`${this.baseUrl}${e}`;return this.post(t,r,i,0)}},h=class{constructor(e){this.options=e,this.queue=[];let r={"openpanel-client-id":e.clientId};e.clientSecret&&(r["openpanel-client-secret"]=e.clientSecret),r["openpanel-sdk"]=e.sdk||"node",r["openpanel-sdk-version"]=e.sdkVersion||"0.0.11-beta",this.api=new d({baseUrl:e.apiUrl||"https://api.openpanel.dev",defaultHeaders:r})}init(){}ready(){this.options.waitForProfile=!1,this.flush()}async send(e){return this.options.filter&&!this.options.filter(e)?Promise.resolve():this.options.waitForProfile&&!this.profileId?(this.queue.push(e),Promise.resolve()):this.api.fetch("/track",e)}setGlobalProperties(e){this.global={...this.global,...e}}async track(e,r){return this.send({type:"track",payload:{name:e,profileId:r?.profileId??this.profileId,properties:{...this.global??{},...r??{}}}})}async identify(e){if(e.profileId&&(this.profileId=e.profileId,this.flush()),Object.keys(e).length>1)return this.send({type:"identify",payload:{...e,properties:{...this.global,...e.properties}}})}async alias(e){return this.send({type:"alias",payload:e})}async increment(e){return this.send({type:"increment",payload:e})}async decrement(e){return this.send({type:"decrement",payload:e})}clear(){this.profileId=void 0}flush(){this.queue.forEach(e=>{this.send({...e,payload:{...e.payload,profileId:this.profileId}})}),this.queue=[]}};function u(e){return e.replace(/([-_][a-z])/gi,r=>r.toUpperCase().replace("-","").replace("_",""))}var c=class extends h{constructor(i){super({...i,sdk:"web",sdkVersion:"0.0.11-beta"});this.options=i;this.lastPath="";this.isServer()||(this.setGlobalProperties({__referrer:document.referrer}),this.options.trackScreenViews&&this.trackScreenViews(),this.options.trackOutgoingLinks&&this.trackOutgoingLinks(),this.options.trackAttributes&&this.trackAttributes())}debounce(i,t){clearTimeout(this.debounceTimer),this.debounceTimer=setTimeout(i,t)}isServer(){return typeof document>"u"}trackOutgoingLinks(){this.isServer()||document.addEventListener("click",i=>{let t=i.target,s=t.closest("a");if(s&&t){let n=s.getAttribute("href");n?.startsWith("http")&&super.track("link_out",{href:n,text:s.innerText||s.getAttribute("title")||t.getAttribute("alt")||t.getAttribute("title")})}})}trackScreenViews(){if(this.isServer())return;this.screenView();let i=history.pushState;history.pushState=function(...a){let o=i.apply(this,a);return window.dispatchEvent(new Event("pushstate")),window.dispatchEvent(new Event("locationchange")),o};let t=history.replaceState;history.replaceState=function(...a){let o=t.apply(this,a);return window.dispatchEvent(new Event("replacestate")),window.dispatchEvent(new Event("locationchange")),o},window.addEventListener("popstate",function(){window.dispatchEvent(new Event("locationchange"))});let s=()=>this.debounce(()=>this.screenView(),50);this.options.trackHashChanges?window.addEventListener("hashchange",s):window.addEventListener("locationchange",s)}trackAttributes(){this.isServer()||document.addEventListener("click",i=>{let t=i.target,s=t.closest("button"),n=t.closest("a"),a=s?.getAttribute("data-event")?s:n?.getAttribute("data-event")?n:null;if(a){let o={};for(let l of a.attributes)l.name.startsWith("data-")&&l.name!=="data-event"&&(o[u(l.name.replace(/^data-/,""))]=l.value);let p=a.getAttribute("data-event");p&&super.track(p,o)}})}screenView(i,t){if(this.isServer())return;let s,n;typeof i=="string"?(s=i,n=t):(s=window.location.href,n=i),this.lastPath!==s&&(this.lastPath=s,super.track("screen_view",{...n??{},__path:s,__title:document.title}))}};(e=>{if(e.op&&"q"in e.op){let r=e.op.q||[],i=new c(r.shift()[1]);r.forEach(t=>{t[0]in i&&i[t[0]](...t.slice(1))}),e.op=(t,...s)=>{let n=i[t]?i[t].bind(i):void 0;typeof n=="function"?n(...s):console.warn(`op.js: ${t} is not a function`)},e.openpanel=i}})(window);})();
//# sourceMappingURL=tracker.global.js.map

View File

@@ -8,7 +8,6 @@
"module": "index.ts", "module": "index.ts",
"scripts": { "scripts": {
"db:codegen": "pnpm -r --filter db run codegen", "db:codegen": "pnpm -r --filter db run codegen",
"js:codegen": "pnpm -r --filter @openpanel/web run build-for-openpanel",
"migrate": "pnpm -r --filter db run migrate", "migrate": "pnpm -r --filter db run migrate",
"migrate:deploy": "pnpm -r --filter db run migrate:deploy", "migrate:deploy": "pnpm -r --filter db run migrate:deploy",
"dev": "pnpm -r --parallel testing", "dev": "pnpm -r --parallel testing",

View File

@@ -1,68 +1,21 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
const VALID_PATHS = [
'/profile',
'/profile/increment',
'/profile/decrement',
'/event',
];
function getIp(req: Request) {
if (req.headers.get('X-Forwarded-For')) {
return req.headers.get('X-Forwarded-For')?.split(',')[0];
}
return req.headers.get('x-real-ip') ?? '0.0.0.0';
}
function getPath(params?: Record<string, unknown>) {
const segments = params?.op;
if (segments && Array.isArray(segments)) {
const path = `/${segments.join('/')}`;
if (VALID_PATHS.includes(path)) {
return path;
}
}
return null;
}
export function createNextRouteHandler({ export function createNextRouteHandler({
clientId, apiUrl = 'https://api.openpanel.dev',
clientSecret,
url = 'https://api.openpanel.dev',
}: { }: {
clientId: string; apiUrl?: string;
clientSecret: string;
url?: string;
}) { }) {
return { return async function POST(req: Request) {
POST: async function POST( const headers = new Headers(req.headers);
req: Request, try {
{ params }: { params: Record<string, unknown> } const res = await fetch(`${apiUrl}/track`, {
) { method: 'POST',
const path = getPath(params); headers,
if (!path) { body: JSON.stringify(await req.json()),
return NextResponse.json('Invalid path'); });
} return NextResponse.json(await res.text());
} catch (e) {
const headers = { return NextResponse.json(e);
'user-agent': req.headers.get('user-agent')!, }
'Content-Type': req.headers.get('Content-Type')!,
'openpanel-client-id': clientId,
'openpanel-client-secret': clientSecret,
'x-client-ip': getIp(req)!,
};
try {
const res = await fetch(`${url}${path}`, {
method: 'POST',
headers,
body: JSON.stringify(await req.json()),
});
return NextResponse.json(await res.text());
} catch (e) {
return NextResponse.json(e);
}
},
}; };
} }

View File

@@ -1,4 +1,6 @@
import React from 'react'; 'use client';
import React, { useEffect } from 'react';
import Script from 'next/script'; import Script from 'next/script';
import type { import type {
@@ -11,13 +13,27 @@ import type {
} from '@openpanel/web'; } from '@openpanel/web';
export * from '@openpanel/web'; export * from '@openpanel/web';
export { createNextRouteHandler } from './createNextRouteHandler';
const CDN_URL = 'https://openpanel.dev/op.js'; const CDN_URL = 'https://openpanel.dev/op.js';
type OpenPanelComponentProps = OpenPanelOptions & { type OpenPanelComponentProps = Omit<OpenPanelOptions, 'filter'> & {
profileId?: string; profileId?: string;
cdnUrl?: string; cdnUrl?: string;
filter?: string;
};
const stringify = (obj: unknown) => {
if (typeof obj === 'object' && obj !== null && obj !== undefined) {
const entries = Object.entries(obj).map(([key, value]) => {
if (key === 'filter') {
return `"${key}":${value}`;
}
return `"${key}":${JSON.stringify(value)}`;
});
return `{${entries.join(',')}}`;
}
return JSON.stringify(obj);
}; };
export function OpenPanelComponent({ export function OpenPanelComponent({
@@ -51,7 +67,7 @@ export function OpenPanelComponent({
__html: `window.op = window.op || function(...args) {(window.op.q = window.op.q || []).push(args)}; __html: `window.op = window.op || function(...args) {(window.op.q = window.op.q || []).push(args)};
${methods ${methods
.map((method) => { .map((method) => {
return `window.op('${method.name}', ${JSON.stringify(method.value)});`; return `window.op('${method.name}', ${stringify(method.value)});`;
}) })
.join('\n')}`, .join('\n')}`,
}} }}
@@ -67,7 +83,7 @@ export function IdentifyComponent(props: IdentifyComponentProps) {
<> <>
<Script <Script
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: `window.op('setProfile', ${JSON.stringify(props)});`, __html: `window.op('identify', ${JSON.stringify(props)});`,
}} }}
/> />
</> </>

View File

@@ -0,0 +1 @@
export { createNextRouteHandler } from './createNextRouteHandler';

View File

@@ -4,6 +4,6 @@ import config from '@openpanel/tsconfig/tsup.config.json' assert { type: 'json'
export default defineConfig({ export default defineConfig({
...(config as any), ...(config as any),
entry: ['index.tsx'], entry: ['index.tsx', 'server.ts'],
external: ['react', 'next'], external: ['react', 'next'],
}); });

View File

@@ -4,7 +4,6 @@
"module": "index.ts", "module": "index.ts",
"scripts": { "scripts": {
"build": "rm -rf dist && tsup", "build": "rm -rf dist && tsup",
"build-for-openpanel": "pnpm build && cp dist/src/tracker.global.js ../../../apps/public/public/tracker.js",
"lint": "eslint .", "lint": "eslint .",
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"", "format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"

View File

@@ -149,8 +149,20 @@ function main() {
types: './dist/index.d.ts', types: './dist/index.d.ts',
files: ['dist'], files: ['dist'],
exports: { exports: {
import: './dist/index.js', '.': {
require: './dist/index.cjs', import: './dist/index.js',
require: './dist/index.cjs',
types: './dist/index.d.ts',
},
...(name === '@openpanel/nextjs'
? {
'./server': {
import: './dist/server.js',
require: './dist/server.cjs',
types: './dist/server.d.ts',
},
}
: {}),
}, },
version: nextVersion, version: nextVersion,
dependencies: Object.entries(restPkgJson.dependencies || {}).reduce( dependencies: Object.entries(restPkgJson.dependencies || {}).reduce(
@@ -213,6 +225,12 @@ function main() {
execSync(`npm publish --access=public --registry ${registry}`, { execSync(`npm publish --access=public --registry ${registry}`, {
cwd: workspacePath(packages[dependent]!.localPath), cwd: workspacePath(packages[dependent]!.localPath),
}); });
if (dependent === '@openpanel/web') {
execSync(
`cp ${workspacePath('packages/sdks/web/dist/src/tracker.global.js')} ${workspacePath('./apps/public/public/tracker.js')}`
);
}
}); });
// Restoring package.json // Restoring package.json
@@ -221,17 +239,17 @@ function main() {
.join(' '); .join(' ');
execSync(`git checkout ${filesToRestore}`); execSync(`git checkout ${filesToRestore}`);
}
// // Save new versions only 😈 // // Save new versions only 😈
dependents.forEach((dependent) => { dependents.forEach((dependent) => {
const { nextVersion, localPath, ...restPkgJson } = packages[dependent]!; const { nextVersion, localPath, ...restPkgJson } = packages[dependent]!;
console.log(`🚀 Saving ${dependent} (${nextVersion})`); console.log(`🚀 Saving ${dependent} (${nextVersion})`);
savePackageJson(workspacePath(`${localPath}/package.json`), { savePackageJson(workspacePath(`${localPath}/package.json`), {
...restPkgJson, ...restPkgJson,
version: nextVersion, version: nextVersion,
});
}); });
}); }
console.log('✅ All done!'); console.log('✅ All done!');
} }