From 73a8da54e8e2e07756da084fe66e8a6dfa7e5225 Mon Sep 17 00:00:00 2001 From: Justin Levine <20596508+justinlevinedotme@users.noreply.github.com> Date: Fri, 24 Apr 2026 08:30:02 -0700 Subject: [PATCH] fix(analytics): switch to @openpanel/nextjs SDK - Use OpenPanelComponent for client-side tracking - Replace Umami server-side tracking with OpenPanel - Update client-side track helper for window.op --- apps/docs/app/r/[name]/route.ts | 5 ++-- apps/docs/components/analytics.tsx | 18 ++++++------- apps/docs/lib/analytics.ts | 18 ++++++------- apps/docs/lib/openpanel.ts | 41 ++++++++++++++++++++++------ apps/docs/lib/umami.ts | 43 ------------------------------ apps/docs/package.json | 2 +- pnpm-lock.yaml | 20 +++++++++++--- 7 files changed, 71 insertions(+), 76 deletions(-) delete mode 100644 apps/docs/lib/umami.ts diff --git a/apps/docs/app/r/[name]/route.ts b/apps/docs/app/r/[name]/route.ts index e45d10e..82f4c13 100644 --- a/apps/docs/app/r/[name]/route.ts +++ b/apps/docs/app/r/[name]/route.ts @@ -1,7 +1,7 @@ /** * Dynamic registry route with download tracking. * - * Serves registry items at /r/[name].json with Umami event tracking. + * Serves registry items at /r/[name].json with OpenPanel event tracking. * Uses generateStaticParams for build-time route generation while * still running tracking code at request time. */ @@ -9,7 +9,7 @@ import { type NextRequest, NextResponse } from "next/server" import { notFound } from "next/navigation" import { getAllItemNames, buildRegistryItemResponse } from "@/lib/registry" -import { trackEvent } from "@/lib/umami" +import { trackEvent } from "@/lib/openpanel" interface RouteParams { params: Promise<{ name: string }> @@ -31,7 +31,6 @@ export async function GET(_request: NextRequest, { params }: RouteParams) { if (process.env.NODE_ENV === "production") { trackEvent({ name: "registry-download", - url: `/r/${name}`, data: { component: itemName }, }) } diff --git a/apps/docs/components/analytics.tsx b/apps/docs/components/analytics.tsx index 23090f9..6925e53 100644 --- a/apps/docs/components/analytics.tsx +++ b/apps/docs/components/analytics.tsx @@ -1,14 +1,14 @@ "use client" -import { useEffect } from "react" -import { op } from "@/lib/openpanel" +import { OpenPanelComponent } from "@openpanel/nextjs" export function Analytics() { - useEffect(() => { - // OpenPanel auto-tracks screen views, outgoing links, and data-track attributes - // Accessing op ensures the client is initialized on mount - void op - }, []) - - return null + return ( + + ) } diff --git a/apps/docs/lib/analytics.ts b/apps/docs/lib/analytics.ts index c24a854..ad837bd 100644 --- a/apps/docs/lib/analytics.ts +++ b/apps/docs/lib/analytics.ts @@ -1,13 +1,13 @@ -declare global { - interface Window { - umami?: { - track: (event: string, properties?: Record) => void - } - } -} +/** + * Client-side analytics helper for OpenPanel. + * + * Provides a simple track function that can be called from anywhere. + * Uses the global OpenPanel instance injected by OpenPanelComponent. + */ export function track(event: string, properties?: Record) { - if (typeof window !== "undefined" && window.umami) { - window.umami.track(event, properties) + if (typeof window !== "undefined" && "op" in window) { + const op = window.op as { track: (event: string, properties?: Record) => void } + op.track(event, properties) } } diff --git a/apps/docs/lib/openpanel.ts b/apps/docs/lib/openpanel.ts index fec5822..6b4efe0 100644 --- a/apps/docs/lib/openpanel.ts +++ b/apps/docs/lib/openpanel.ts @@ -1,8 +1,33 @@ -import { OpenPanel } from "@openpanel/web" - -export const op = new OpenPanel({ - clientId: process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID ?? "", - trackScreenViews: true, - trackOutgoingLinks: true, - trackAttributes: true, -}) +/** + * OpenPanel server-side tracking for registry downloads. + * + * Sends events to OpenPanel without blocking the response. + * Requires NEXT_PUBLIC_OPENPANEL_CLIENT_ID and OPENPANEL_CLIENT_SECRET env vars. + */ + +import { OpenPanel } from "@openpanel/nextjs" + +const clientId = process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID +const clientSecret = process.env.OPENPANEL_CLIENT_SECRET + +export const opServer = + clientId && clientSecret + ? new OpenPanel({ clientId, clientSecret }) + : null + +interface TrackEventOptions { + name: string + data?: Record +} + +export async function trackEvent({ name, data }: TrackEventOptions): Promise { + if (!opServer) { + return + } + + try { + await opServer.track(name, data ?? {}) + } catch { + // Silently fail — don't block registry responses for analytics + } +} diff --git a/apps/docs/lib/umami.ts b/apps/docs/lib/umami.ts deleted file mode 100644 index db0602d..0000000 --- a/apps/docs/lib/umami.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Umami server-side tracking for registry downloads. - * - * Sends events to Umami without blocking the response. - * Requires UMAMI_HOST_URL and UMAMI_WEBSITE_ID env vars. - */ - -interface TrackEventOptions { - name: string - url?: string - data?: Record -} - -const UMAMI_HOST_URL = process.env.UMAMI_HOST_URL -const UMAMI_WEBSITE_ID = process.env.UMAMI_WEBSITE_ID - -export async function trackEvent({ name, url, data }: TrackEventOptions): Promise { - if (!UMAMI_HOST_URL || !UMAMI_WEBSITE_ID) { - return - } - - try { - await fetch(`${UMAMI_HOST_URL}/api/send`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "User-Agent": "jalco-ui/registry", - }, - body: JSON.stringify({ - type: "event", - payload: { - website: UMAMI_WEBSITE_ID, - hostname: "ui.justinlevine.me", - url: url ?? "/", - name, - data, - }, - }), - }) - } catch { - // Silently fail — don't block registry responses for analytics - } -} diff --git a/apps/docs/package.json b/apps/docs/package.json index bbcd11e..2731e76 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "@chenglou/pretext": "^0.0.4", - "@openpanel/web": "^1.4.0", + "@openpanel/nextjs": "^1.5.0", "@orama/orama": "^3.1.18", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-slot": "^1.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c8714b..8a6746b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,9 +17,9 @@ importers: '@chenglou/pretext': specifier: ^0.0.4 version: 0.0.4 - '@openpanel/web': - specifier: ^1.4.0 - version: 1.4.0 + '@openpanel/nextjs': + specifier: ^1.5.0 + version: 1.5.0(next@16.2.1(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@orama/orama': specifier: ^3.1.18 version: 3.1.18 @@ -909,6 +909,13 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@openpanel/nextjs@1.5.0': + resolution: {integrity: sha512-CuEUdYb8LZxrsoO1+6EXKaCp6tJtEeQzStaEUVBwpewV50mp0pOf3JOboH4K61nIBy3CDLLpbnzUw3iMgrqSYQ==} + 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 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@openpanel/sdk@1.3.0': resolution: {integrity: sha512-VK/1oawBjGdxA+oYtqcWlNXlLT1zRJ9tslHoMvqqsqlcLNOhH26ltcHpyGp5RhtIF7uIkCltiicALfFN7fyldw==} @@ -5684,6 +5691,13 @@ snapshots: '@open-draft/until@2.1.0': {} + '@openpanel/nextjs@1.5.0(next@16.2.1(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@openpanel/web': 1.4.0 + next: 16.2.1(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + '@openpanel/sdk@1.3.0': {} '@openpanel/web@1.4.0':