Skip to content

posthog migration #7340

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions apps/dashboard/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,6 @@ NEXT_PUBLIC_DASHBOARD_UPLOAD_SERVER="https://storage.thirdweb-dev.com"
# - not required to build (unless testing contract search)
NEXT_PUBLIC_TYPESENSE_CONTRACT_API_KEY=

# posthog API key
# - not required for prod/staging
NEXT_PUBLIC_POSTHOG_API_KEY="ignored"

# Stripe Customer portal
NEXT_PUBLIC_STRIPE_KEY=

Expand Down Expand Up @@ -108,4 +104,8 @@ STRIPE_SECRET_KEY=""

# required for server wallet management
NEXT_PUBLIC_THIRDWEB_VAULT_URL=""
NEXT_PUBLIC_ENGINE_CLOUD_URL=""
NEXT_PUBLIC_ENGINE_CLOUD_URL=""

# posthog setup
NEXT_PUBLIC_POSTHOG_KEY=""
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
12 changes: 12 additions & 0 deletions apps/dashboard/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ module.exports = {
message:
'This is likely a mistake. If you really want to import this - postfix the imported name with Icon. Example - "LinkIcon"',
},
{
name: "posthog-js",
message:
'Import "posthog-js" directly only within the analytics helpers ("src/@/analytics/*"). Use the exported helpers from "@/analytics/track" elsewhere.',
},
],
},
],
Expand Down Expand Up @@ -139,6 +144,13 @@ module.exports = {
"no-restricted-imports": ["off"],
},
},
// allow direct PostHog imports inside analytics helpers
{
files: "src/@/analytics/**/*",
rules: {
"no-restricted-imports": ["off"],
},
},
Comment on lines +147 to +153
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Don’t disable all import restrictions in analytics helpers

Turning no-restricted-imports completely off for "src/@/analytics/**/*" re-enables every restricted import (e.g. direct Chakra-UI components, next/navigation’s useRouter, etc.) inside that folder.
We only need to lift the ban for posthog-js; the other guards should keep protecting the codebase.

Consider a narrower override that keeps the rest of the rule active:

{
  files: "src/@/analytics/**/*",
  rules: {
-   "no-restricted-imports": ["off"],
+   "no-restricted-imports": [
+     "error",
+     {
+       paths: [
+         // Copy the existing paths **except** the posthog-js entry,
+         // so the other restrictions remain enforced.
+       ],
+     },
+   ],
  },
},

This preserves the architectural discipline for all other libraries while still allowing direct posthog-js imports where they’re meant to live.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/dashboard/.eslintrc.js around lines 147 to 153, the current override
disables all import restrictions for files in src/@/analytics/**/*, which
unintentionally allows all restricted imports. Instead, modify the override to
only allow direct imports of 'posthog-js' by configuring the
no-restricted-imports rule to permit 'posthog-js' while keeping other
restrictions active. This ensures only the intended exception is made without
removing protections for other restricted imports.

// enable rule specifically for TypeScript files
{
files: ["*.ts", "*.tsx"],
Expand Down
17 changes: 17 additions & 0 deletions apps/dashboard/instrumentation-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import posthog from "posthog-js";

const NEXT_PUBLIC_POSTHOG_KEY = process.env.NEXT_PUBLIC_POSTHOG_KEY;

if (NEXT_PUBLIC_POSTHOG_KEY) {
posthog.init(NEXT_PUBLIC_POSTHOG_KEY, {
api_host: "/_ph",
ui_host: "https://us.posthog.com",
capture_pageview: "history_change",
capture_pageleave: "if_capture_pageview",
// disable exception capture (for now)
capture_exceptions: false,
// specifically disable autocapture (does not affect pageview capture)
autocapture: false,
debug: process.env.NODE_ENV === "development",
});
}
3 changes: 2 additions & 1 deletion apps/dashboard/knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@thirdweb-dev/service-utils",
"@thirdweb-dev/vault-sdk",
"@types/color",
"fast-xml-parser"
"fast-xml-parser",
"posthog-node"
]
}
14 changes: 14 additions & 0 deletions apps/dashboard/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,18 @@ const baseNextConfig: NextConfig = {
},
async rewrites() {
return [
{
source: "/_ph/static/:path*",
destination: "https://us-assets.i.posthog.com/static/:path*",
},
{
source: "/_ph/:path*",
destination: "https://us.i.posthog.com/:path*",
},
{
source: "/_ph/decide",
destination: "https://us.i.posthog.com/decide",
},
{
source: "/thirdweb.eth",
destination: "/deployer.thirdweb.eth",
Expand Down Expand Up @@ -173,6 +185,8 @@ const baseNextConfig: NextConfig = {
]),
];
},
// This is required to support PostHog trailing slash API requests
skipTrailingSlashRedirect: true,
images: {
dangerouslyAllowSVG: true,
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
Expand Down
4 changes: 2 additions & 2 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@
"date-fns": "4.1.0",
"fast-xml-parser": "^5.2.5",
"fetch-event-stream": "0.1.5",
"flat": "^6.0.1",
"framer-motion": "12.17.0",
"fuse.js": "7.1.0",
"input-otp": "^1.4.1",
Expand All @@ -78,7 +77,8 @@
"p-limit": "^6.2.0",
"papaparse": "^5.5.3",
"pluralize": "^8.0.0",
"posthog-js": "1.67.1",
"posthog-js": "1.252.0",
"posthog-node": "5.1.0",
"prettier": "3.5.3",
"qrcode": "^1.5.3",
"react": "19.1.0",
Expand Down
56 changes: 56 additions & 0 deletions apps/dashboard/src/@/analytics/dashboard.client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"use client";

import posthog from "posthog-js";
import { useEffect } from "react";
import type { Account } from "../../@3rdweb-sdk/react/hooks/useApi";

const warnedMessages = new Set<string>();
function warnOnce(message: string) {
if (warnedMessages.has(message)) {
return;
}
warnedMessages.add(message);
console.warn(message);
}

export function AccountIdentifier(props: {
account: Pick<Account, "id" | "email">;
}) {
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
if (!posthog.__loaded) {
warnOnce(
"[DASHBOARD_ANALYTICS] is not initialized, cannot identify user",
);
return;
}
posthog.identify(props.account.id, {
...(props.account.email ? { email: props.account.email } : {}),
});
}, [props.account.id, props.account.email]);
return null;
}

export function TeamIdentifier(props: {
teamId: string;
}) {
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
if (!posthog.__loaded) {
warnOnce(
"[DASHBOARD_ANALYTICS] is not initialized, cannot identify team",
);
return;
}
posthog.group("team", props.teamId);
}, [props.teamId]);
return null;
}

export function reset() {
if (!posthog.__loaded) {
warnOnce("[DASHBOARD_ANALYTICS] is not initialized, cannot reset");
return;
}
posthog.reset();
}
33 changes: 33 additions & 0 deletions apps/dashboard/src/@/analytics/track.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Central entry point for analytics tracking helpers.
*
* This file re-exports individual event *categories* located under
* `./track/*`. Each category file in turn exposes small helper functions that
* call PostHog with type-safe payloads.
*
* Example – reporting a contract deployment:
* ```ts
* import { reportContractDeployed } from "@/analytics/track";
*
* reportContractDeployed({
* address: "0x…",
* chainId: 1,
* });
* ```
*
* Adding a new event *category*:
* 1. Create a new file under `./track/<category>.ts`.
* 2. Inside that file, follow the pattern shown in `contract.ts` to define
* Zod schemas + `reportWhatever` helpers.
* 3. Add a star-export below so the helpers are surfaced at the package root:
* ```ts
* export * from "./track/<category>";
* ```
* 4. Consumers can now `import { reportMyEvent } from "@/analytics/track";`.
*/

export * from "./track/contract";
export * from "./track/token";
export * from "./track/nft";
export * from "./track/onboarding";
export * from "./track/marketplace";
106 changes: 106 additions & 0 deletions apps/dashboard/src/@/analytics/track/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Analytics Tracking Helpers

This folder contains **type-safe wrappers** around our analytics provider (PostHog).
Instead of calling `posthog.capture` directly, feature code should import the
pre-defined helpers from here. This guarantees that:

1. Event names are consistent.
2. Payloads adhere to a strict schema (validated at runtime via Zod and typed at
compile time).

---

## Quick start

```ts
import { reportContractDeployed } from "@/analytics/track";

// Contract deployment example
reportContractDeployed({
address: "0x…",
chainId: 1,
});
```

> **Note** Ensure that PostHog is initialised *before* you emit events. Our
> bootstrapping code does this automatically during app start-up.

---

## Project structure

```
track/ # ← you are here
├─ __internal.ts # low-level wrapper around posthog.capture (do NOT use)
├─ contract.ts # "contract" event category helpers
├─ README.md # this file
└─ … # future categories live here
```

* `__internal.ts` exposes `__internal__reportEvent` which performs the actual
PostHog call and safeguards against the SDK not being ready.
* Every **category file** (such as `contract.ts`) groups together related events.
Each event is represented by a `report<Something>` function.
* `track.ts` sits one directory up and **re-exports all helper functions** so
consumers can import the ones they need directly:

```ts
import { reportContractDeployed /*, reportOtherEvent */ } from "@/analytics/track";
```

---

## Adding a **new event** to an existing category

1. Open the relevant category file (e.g. `contract.ts`).
2. Define a new Zod schema describing the payload:

```ts
const ContractUpgradeSchema = z.object({
address: z.string(),
oldVersion: z.string(),
newVersion: z.string(),
});
```

3. Export a reporting helper that forwards the validated payload:

```ts
export function reportContractUpgraded(
payload: z.infer<typeof ContractUpgradeSchema>,
) {
__internal__reportEvent("contract upgraded", payload);
}
```

That's it – consumers can now call `track.contract.reportContractUpgraded(...)`.

---

## Adding a **new category**

1. Create a new file `track/<category>.ts`.
2. Follow the same pattern as in `contract.ts` (define schemas + export helper
functions).
3. Open `track.ts` (one directory up) and add a star-export so the helpers are
surfaced at the package root:

```ts
export * from "./track/<category>";
```

Your new helpers can now be imported directly:

```ts
import { reportMyNewEvent } from "@/analytics/track";
```

---

## Conventions & best practices

* **Lowercase, space-separated event names** – keep them human-readable.
* **Small, focused payloads** – only include properties that are useful for
analytics.
* **Avoid calling PostHog directly** – always go through `__internal__reportEvent`
so we keep a single choke-point.
23 changes: 23 additions & 0 deletions apps/dashboard/src/@/analytics/track/__internal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import posthog, { type Properties } from "posthog-js";

/**
* Low-level wrapper around `posthog.capture` with a safety-check that waits
* until PostHog has been initialised.
*
* **⚠️ INTERNAL USE ONLY** – Do not call this directly from feature code.
* Instead, create a domain-specific helper (see `contract.ts`) so we keep a
* single source of truth for event names & payload contracts.
*/

export function __internal__reportEvent(
eventName: string,
properties?: Properties | null,
) {
if (!posthog.__loaded) {
console.warn(
"[DASHBOARD_ANALYTICS] is not initialized, cannot track event",
);
return;
Comment on lines +16 to +20
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

posthog.__loaded is not in the public typings – this will break tsc.

posthog-js’s type definitions do not expose the __loaded flag, so accessing it directly fails strict compilation (Property '__loaded' does not exist on type 'PostHog'). Cast or guard with an index signature to silence the error without disabling noImplicitAny.

-  if (!posthog.__loaded) {
+  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
+  if (!(posthog as unknown as { __loaded?: boolean }).__loaded) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!posthog.__loaded) {
console.warn(
"[DASHBOARD_ANALYTICS] is not initialized, cannot track event",
);
return;
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
if (!(posthog as unknown as { __loaded?: boolean }).__loaded) {
console.warn(
"[DASHBOARD_ANALYTICS] is not initialized, cannot track event",
);
return;
🤖 Prompt for AI Agents
In apps/dashboard/src/@/analytics/track/__internal.ts around lines 16 to 20,
accessing the private property posthog.__loaded causes TypeScript compilation
errors because it is not in the public typings. To fix this, use a type
assertion or an index signature to access __loaded safely, such as casting
posthog to any or using (posthog as any).__loaded, so the compiler does not
complain while preserving type safety without disabling noImplicitAny.

}
posthog.capture(eventName, properties);
}
Loading
Loading