From 2226cb463dc052a763542da31d393e9486ddeebd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl-Gerhard=20Lindesva=CC=88rd?= Date: Thu, 1 Aug 2024 22:01:46 +0200 Subject: [PATCH] wip --- apps/api/src/index.ts | 2 + apps/api/src/routes/track.router.ts | 6 +- apps/public/public/tracker.js | 2 + package.json | 1 - .../sdks/nextjs/createNextRouteHandler.ts | 75 ++++--------------- packages/sdks/nextjs/index.tsx | 26 +++++-- packages/sdks/nextjs/server.ts | 1 + packages/sdks/nextjs/tsup.config.ts | 2 +- packages/sdks/web/package.json | 1 - tooling/publish/publish.ts | 40 +++++++--- 10 files changed, 73 insertions(+), 83 deletions(-) create mode 100644 apps/public/public/tracker.js create mode 100644 packages/sdks/nextjs/server.ts diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 69bc5bf5..a898c8ac 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -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'); }); diff --git a/apps/api/src/routes/track.router.ts b/apps/api/src/routes/track.router.ts index 8829829f..3cedf61e 100644 --- a/apps/api/src/routes/track.router.ts +++ b/apps/api/src/routes/track.router.ts @@ -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; diff --git a/apps/public/public/tracker.js b/apps/public/public/tracker.js new file mode 100644 index 00000000..070a2779 --- /dev/null +++ b/apps/public/public/tracker.js @@ -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(tsetTimeout(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 \ No newline at end of file diff --git a/package.json b/package.json index f5e8b07a..5726d241 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/sdks/nextjs/createNextRouteHandler.ts b/packages/sdks/nextjs/createNextRouteHandler.ts index f65d0612..fab90651 100644 --- a/packages/sdks/nextjs/createNextRouteHandler.ts +++ b/packages/sdks/nextjs/createNextRouteHandler.ts @@ -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) { - 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 } - ) { - 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); + } }; } diff --git a/packages/sdks/nextjs/index.tsx b/packages/sdks/nextjs/index.tsx index 7c83df30..e42bf4ee 100644 --- a/packages/sdks/nextjs/index.tsx +++ b/packages/sdks/nextjs/index.tsx @@ -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 & { 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) { <>