wip
This commit is contained in:
committed by
Carl-Gerhard Lindesvärd
parent
15e997129a
commit
2226cb463d
@@ -22,6 +22,7 @@ import importRouter from './routes/import.router';
|
||||
import liveRouter from './routes/live.router';
|
||||
import miscRouter from './routes/misc.router';
|
||||
import profileRouter from './routes/profile.router';
|
||||
import trackRouter from './routes/track.router';
|
||||
import webhookRouter from './routes/webhook.router';
|
||||
import { logger, logInfo } from './utils/logger';
|
||||
|
||||
@@ -135,6 +136,7 @@ const startServer = async () => {
|
||||
fastify.register(exportRouter, { prefix: '/export' });
|
||||
fastify.register(webhookRouter, { prefix: '/webhook' });
|
||||
fastify.register(importRouter, { prefix: '/import' });
|
||||
fastify.register(trackRouter, { prefix: '/track' });
|
||||
fastify.setErrorHandler((error) => {
|
||||
logger.error(error, 'Error in request');
|
||||
});
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { isBot } from '@/bots';
|
||||
import type { TrackHandlerPayload } from '@/controllers/track.controller';
|
||||
import { handler } from '@/controllers/track.controller';
|
||||
import { SdkAuthError, validateSdkRequest } from '@/utils/auth';
|
||||
import { logger } from '@/utils/logger';
|
||||
import type { FastifyPluginCallback, FastifyRequest } from 'fastify';
|
||||
|
||||
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(
|
||||
'preHandler',
|
||||
async (
|
||||
@@ -66,4 +66,4 @@ const eventRouter: FastifyPluginCallback = (fastify, opts, done) => {
|
||||
done();
|
||||
};
|
||||
|
||||
export default eventRouter;
|
||||
export default trackRouter;
|
||||
|
||||
2
apps/public/public/tracker.js
Normal file
2
apps/public/public/tracker.js
Normal 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
|
||||
@@ -8,7 +8,6 @@
|
||||
"module": "index.ts",
|
||||
"scripts": {
|
||||
"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:deploy": "pnpm -r --filter db run migrate:deploy",
|
||||
"dev": "pnpm -r --parallel testing",
|
||||
|
||||
@@ -1,68 +1,21 @@
|
||||
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({
|
||||
clientId,
|
||||
clientSecret,
|
||||
url = 'https://api.openpanel.dev',
|
||||
apiUrl = 'https://api.openpanel.dev',
|
||||
}: {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
url?: string;
|
||||
apiUrl?: string;
|
||||
}) {
|
||||
return {
|
||||
POST: async function POST(
|
||||
req: Request,
|
||||
{ params }: { params: Record<string, unknown> }
|
||||
) {
|
||||
const path = getPath(params);
|
||||
if (!path) {
|
||||
return NextResponse.json('Invalid path');
|
||||
}
|
||||
|
||||
const headers = {
|
||||
'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);
|
||||
}
|
||||
},
|
||||
return async function POST(req: Request) {
|
||||
const headers = new Headers(req.headers);
|
||||
try {
|
||||
const res = await fetch(`${apiUrl}/track`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(await req.json()),
|
||||
});
|
||||
return NextResponse.json(await res.text());
|
||||
} catch (e) {
|
||||
return NextResponse.json(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
'use client';
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import Script from 'next/script';
|
||||
|
||||
import type {
|
||||
@@ -11,13 +13,27 @@ import type {
|
||||
} from '@openpanel/web';
|
||||
|
||||
export * from '@openpanel/web';
|
||||
export { createNextRouteHandler } from './createNextRouteHandler';
|
||||
|
||||
const CDN_URL = 'https://openpanel.dev/op.js';
|
||||
|
||||
type OpenPanelComponentProps = OpenPanelOptions & {
|
||||
type OpenPanelComponentProps = Omit<OpenPanelOptions, 'filter'> & {
|
||||
profileId?: 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({
|
||||
@@ -51,7 +67,7 @@ export function OpenPanelComponent({
|
||||
__html: `window.op = window.op || function(...args) {(window.op.q = window.op.q || []).push(args)};
|
||||
${methods
|
||||
.map((method) => {
|
||||
return `window.op('${method.name}', ${JSON.stringify(method.value)});`;
|
||||
return `window.op('${method.name}', ${stringify(method.value)});`;
|
||||
})
|
||||
.join('\n')}`,
|
||||
}}
|
||||
@@ -67,7 +83,7 @@ export function IdentifyComponent(props: IdentifyComponentProps) {
|
||||
<>
|
||||
<Script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `window.op('setProfile', ${JSON.stringify(props)});`,
|
||||
__html: `window.op('identify', ${JSON.stringify(props)});`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
1
packages/sdks/nextjs/server.ts
Normal file
1
packages/sdks/nextjs/server.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { createNextRouteHandler } from './createNextRouteHandler';
|
||||
@@ -4,6 +4,6 @@ import config from '@openpanel/tsconfig/tsup.config.json' assert { type: 'json'
|
||||
|
||||
export default defineConfig({
|
||||
...(config as any),
|
||||
entry: ['index.tsx'],
|
||||
entry: ['index.tsx', 'server.ts'],
|
||||
external: ['react', 'next'],
|
||||
});
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"module": "index.ts",
|
||||
"scripts": {
|
||||
"build": "rm -rf dist && tsup",
|
||||
"build-for-openpanel": "pnpm build && cp dist/src/tracker.global.js ../../../apps/public/public/tracker.js",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
|
||||
"typecheck": "tsc --noEmit"
|
||||
|
||||
@@ -149,8 +149,20 @@ function main() {
|
||||
types: './dist/index.d.ts',
|
||||
files: ['dist'],
|
||||
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,
|
||||
dependencies: Object.entries(restPkgJson.dependencies || {}).reduce(
|
||||
@@ -213,6 +225,12 @@ function main() {
|
||||
execSync(`npm publish --access=public --registry ${registry}`, {
|
||||
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
|
||||
@@ -221,17 +239,17 @@ function main() {
|
||||
.join(' ');
|
||||
|
||||
execSync(`git checkout ${filesToRestore}`);
|
||||
}
|
||||
|
||||
// // Save new versions only 😈
|
||||
dependents.forEach((dependent) => {
|
||||
const { nextVersion, localPath, ...restPkgJson } = packages[dependent]!;
|
||||
console.log(`🚀 Saving ${dependent} (${nextVersion})`);
|
||||
savePackageJson(workspacePath(`${localPath}/package.json`), {
|
||||
...restPkgJson,
|
||||
version: nextVersion,
|
||||
// // Save new versions only 😈
|
||||
dependents.forEach((dependent) => {
|
||||
const { nextVersion, localPath, ...restPkgJson } = packages[dependent]!;
|
||||
console.log(`🚀 Saving ${dependent} (${nextVersion})`);
|
||||
savePackageJson(workspacePath(`${localPath}/package.json`), {
|
||||
...restPkgJson,
|
||||
version: nextVersion,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
console.log('✅ All done!');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user