@curvenote/analytics-posthog 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +178 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/posthog-analytics-boundary.d.ts +15 -0
- package/dist/posthog-analytics-boundary.d.ts.map +1 -0
- package/dist/posthog-analytics-boundary.js +19 -0
- package/dist/posthog-client.d.ts +32 -0
- package/dist/posthog-client.d.ts.map +1 -0
- package/dist/posthog-client.js +62 -0
- package/dist/property-keys.d.ts +4 -0
- package/dist/property-keys.d.ts.map +1 -0
- package/dist/property-keys.js +18 -0
- package/dist/use-analytics-page-view.d.ts +13 -0
- package/dist/use-analytics-page-view.d.ts.map +1 -0
- package/dist/use-analytics-page-view.js +25 -0
- package/dist/use-posthog-site-registration.d.ts +13 -0
- package/dist/use-posthog-site-registration.d.ts.map +1 -0
- package/dist/use-posthog-site-registration.js +12 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# @curvenote/analytics-posthog
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@curvenote/analytics-posthog)
|
|
4
|
+
[](https://github.com/curvenote/curvenote/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
Shared PostHog client integration for Curvenote React Router apps. Wires the `posthog-js` singleton into `@curvenote/theme-ui`’s `AnalyticsBoundary` so `useAnalytics()` calls from theme UI and renderers reach PostHog reliably.
|
|
7
|
+
|
|
8
|
+
Used by `apps/theme` (`app: scms-theme`) and `apps/openrxiv` (`app: openrxiv-reader`). See [docs/analytics.md](../../docs/analytics.md) for the full monorepo setup guide.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
bun add @curvenote/analytics-posthog posthog-js @posthog/react @curvenote/theme-ui
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Peer dependencies: `react`, `react-dom`, `react-router`, `@curvenote/theme-ui`.
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
### 1. Vite build-time config
|
|
21
|
+
|
|
22
|
+
Load PostHog credentials from app-config and expose them to the client bundle (do not import secrets via `@app-config/main` in client code):
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
// vite.config.ts
|
|
26
|
+
const posthogProjectToken = analytics.posthog?.projectToken ?? '';
|
|
27
|
+
const posthogApiHost = analytics.posthog?.apiHost ?? '';
|
|
28
|
+
|
|
29
|
+
export default defineConfig({
|
|
30
|
+
define: {
|
|
31
|
+
__POSTHOG_PROJECT_TOKEN__: JSON.stringify(posthogProjectToken),
|
|
32
|
+
__POSTHOG_API_HOST__: JSON.stringify(posthogApiHost),
|
|
33
|
+
},
|
|
34
|
+
resolve: {
|
|
35
|
+
dedupe: ['@curvenote/theme-ui', '@curvenote/analytics-posthog', 'posthog-js', '@posthog/react', 'react', 'react-dom'],
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Set `APP_CONFIG_ENV` when running `react-router build` so the correct token is inlined.
|
|
41
|
+
|
|
42
|
+
### 2. Client entry
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
// app/entry.client.tsx
|
|
46
|
+
import { initPosthogClient, posthog } from '@curvenote/analytics-posthog';
|
|
47
|
+
import { PostHogProvider } from '@posthog/react';
|
|
48
|
+
|
|
49
|
+
declare const __POSTHOG_PROJECT_TOKEN__: string;
|
|
50
|
+
declare const __POSTHOG_API_HOST__: string;
|
|
51
|
+
|
|
52
|
+
initPosthogClient({
|
|
53
|
+
projectToken: __POSTHOG_PROJECT_TOKEN__,
|
|
54
|
+
apiHost: __POSTHOG_API_HOST__,
|
|
55
|
+
app: 'my-app-id', // PostHog super-property on every event
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
hydrateRoot(
|
|
59
|
+
document,
|
|
60
|
+
<PostHogProvider client={posthog}>
|
|
61
|
+
<HydratedRouter />
|
|
62
|
+
</PostHogProvider>,
|
|
63
|
+
);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 3. Root layout — site super-properties
|
|
67
|
+
|
|
68
|
+
Pass `site` from the root loader into `PostHogRootProvider` / `initPosthogClient` so `site_id`, `site_name`, and `site_private` register synchronously at init (alongside `app`). Optionally keep `usePostHogSiteRegistration({ site })` to re-register if site loader data changes.
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
import { usePostHogSiteRegistration } from '@curvenote/analytics-posthog';
|
|
72
|
+
|
|
73
|
+
function PostHogLayout({ site, posthog, children }) {
|
|
74
|
+
usePostHogSiteRegistration({ site });
|
|
75
|
+
return (
|
|
76
|
+
<PostHogRootProvider config={posthog} site={site}>
|
|
77
|
+
{children}
|
|
78
|
+
</PostHogRootProvider>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 4. Routes — boundary + page views
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { PostHogAnalyticsBoundary, useAnalyticsPageView } from '@curvenote/analytics-posthog';
|
|
87
|
+
import { useAnalytics } from '@curvenote/theme-ui';
|
|
88
|
+
|
|
89
|
+
function PageViewedEvent({ slug }: { slug: string }) {
|
|
90
|
+
useAnalyticsPageView('article_viewed', { slug }, [slug]);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export default function ArticleRoute() {
|
|
95
|
+
const metadata = useMemo(() => ({ workVersionId, doi, /* camelCase */ }), [/* … */]);
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<PostHogAnalyticsBoundary context={metadata}>
|
|
99
|
+
<PageViewedEvent slug={page.slug} />
|
|
100
|
+
{/* MystProviders, PageProviders, ArticleBar, etc. */}
|
|
101
|
+
</PostHogAnalyticsBoundary>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function MyButton() {
|
|
106
|
+
const capture = useAnalytics();
|
|
107
|
+
return <button onClick={() => capture('button_clicked', { surface: 'toolbar' })} />;
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## API
|
|
112
|
+
|
|
113
|
+
### `initPosthogClient(options)`
|
|
114
|
+
|
|
115
|
+
Initialize the shared `posthog-js` instance. Call once from `entry.client.tsx`.
|
|
116
|
+
|
|
117
|
+
| Option | Description |
|
|
118
|
+
|--------|-------------|
|
|
119
|
+
| `projectToken` | Public `phc_*` project token |
|
|
120
|
+
| `apiHost` | Ingestion host (e.g. `https://us.i.posthog.com` or a reverse proxy) |
|
|
121
|
+
| `app` | Value for the `app` super-property (e.g. `scms-theme`) |
|
|
122
|
+
| `site` | Optional root-loader site; registers `site_id`, `site_name`, `site_private` at init |
|
|
123
|
+
|
|
124
|
+
No-op when `projectToken` is empty. Dispatches a `posthog:loaded` custom event when ready (`POSTHOG_LOADED_EVENT`).
|
|
125
|
+
|
|
126
|
+
### `PostHogAnalyticsBoundary`
|
|
127
|
+
|
|
128
|
+
React provider that connects `useAnalytics()` from `@curvenote/theme-ui` to PostHog.
|
|
129
|
+
|
|
130
|
+
- **`context`** — optional route-level metadata (`camelCase`); merged into every event and converted to `snake_case` by `useAnalytics()`.
|
|
131
|
+
- Uses the **posthog-js singleton**, not `usePostHog()`, so capture works before the React provider settles.
|
|
132
|
+
|
|
133
|
+
### `useAnalyticsPageView(event, properties?, deps?, options?)`
|
|
134
|
+
|
|
135
|
+
Fire a custom page-view-style event once per React Router navigation (`location.key`). Dedupes Strict Mode double-mounts.
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
useAnalyticsPageView(
|
|
139
|
+
'submissions_index_viewed',
|
|
140
|
+
{ submissionCount: 10 },
|
|
141
|
+
[submissionCount],
|
|
142
|
+
{ enabled: true }, // optional; default true
|
|
143
|
+
);
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Do **not** gate on `usePostHog().__loaded` — `initPosthogClient()` runs before hydrate and posthog-js queues events until ready.
|
|
147
|
+
|
|
148
|
+
### `usePostHogSiteRegistration({ site })`
|
|
149
|
+
|
|
150
|
+
Re-registers site super-properties when root loader site data changes. Primary registration is at `initPosthogClient()` time via the `site` option.
|
|
151
|
+
|
|
152
|
+
### Low-level helpers
|
|
153
|
+
|
|
154
|
+
| Export | Use when |
|
|
155
|
+
|--------|----------|
|
|
156
|
+
| `capturePosthogBoundaryEvent(event, props)` | Properties already `snake_case` (boundary `capture` fn) |
|
|
157
|
+
| `capturePosthogClientEvent(event, props)` | Direct capture; converts `camelCase` → `snake_case` |
|
|
158
|
+
| `isPosthogClientEnabled()` | Build had a non-empty project token |
|
|
159
|
+
| `posthog` | Same singleton passed to `PostHogProvider` |
|
|
160
|
+
|
|
161
|
+
## Conventions
|
|
162
|
+
|
|
163
|
+
- **Event names** in PostHog: `snake_case` (e.g. `article_viewed`, `submission_card_clicked`).
|
|
164
|
+
- **Properties** passed to `useAnalytics()`: `camelCase`; the theme-ui hook converts to `snake_case`.
|
|
165
|
+
- **Product analytics**: use `useAnalytics()` inside a `PostHogAnalyticsBoundary`.
|
|
166
|
+
- **Exceptions**: `usePostHog().captureException()` in error boundaries is fine.
|
|
167
|
+
|
|
168
|
+
## Vite monorepo notes
|
|
169
|
+
|
|
170
|
+
In development, alias `@curvenote/analytics-posthog` to `packages/analytics-posthog/src/index.ts`. In production, alias to `dist/index.js` after `bun run build` in this package.
|
|
171
|
+
|
|
172
|
+
Also alias `@curvenote/theme-ui` and `@curvenote/renderers` to workspace `src` in dev so a single `AnalyticsBoundary` context is shared (see `resolve.dedupe` above).
|
|
173
|
+
|
|
174
|
+
## Related docs
|
|
175
|
+
|
|
176
|
+
- [docs/analytics.md](../../docs/analytics.md) — monorepo integration checklist
|
|
177
|
+
- [apps/theme/ANALYTICS_INSTRUMENTATION.md](../../apps/theme/ANALYTICS_INSTRUMENTATION.md) — ArticleBar / UI event catalog
|
|
178
|
+
- [apps/openrxiv/ANALYTICS_INSTRUMENTATION.md](../../apps/openrxiv/ANALYTICS_INSTRUMENTATION.md) — OpenRxiv route and server events
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { POSTHOG_LOADED_EVENT, capturePosthogBoundaryEvent, capturePosthogClientEvent, initPosthogClient, isPosthogClientEnabled, posthog, registerPosthogSiteSuperProperties, setPosthogClientEnabled, siteToPosthogSuperProperties, type InitPosthogClientOptions, type PosthogSiteSuperProperties, } from './posthog-client.js';
|
|
2
|
+
export { toSnakeKeys } from './property-keys.js';
|
|
3
|
+
export { PostHogAnalyticsBoundary } from './posthog-analytics-boundary.js';
|
|
4
|
+
export { useAnalyticsPageView, type UseAnalyticsPageViewOptions, } from './use-analytics-page-view.js';
|
|
5
|
+
export { usePostHogSiteRegistration, type PostHogSiteRegistrationArgs, type PostHogSiteRegistrationSite, } from './use-posthog-site-registration.js';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EACpB,2BAA2B,EAC3B,yBAAyB,EACzB,iBAAiB,EACjB,sBAAsB,EACtB,OAAO,EACP,kCAAkC,EAClC,uBAAuB,EACvB,4BAA4B,EAC5B,KAAK,wBAAwB,EAC7B,KAAK,0BAA0B,GAChC,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAC3E,OAAO,EACL,oBAAoB,EACpB,KAAK,2BAA2B,GACjC,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,0BAA0B,EAC1B,KAAK,2BAA2B,EAChC,KAAK,2BAA2B,GACjC,MAAM,oCAAoC,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { POSTHOG_LOADED_EVENT, capturePosthogBoundaryEvent, capturePosthogClientEvent, initPosthogClient, isPosthogClientEnabled, posthog, registerPosthogSiteSuperProperties, setPosthogClientEnabled, siteToPosthogSuperProperties, } from './posthog-client.js';
|
|
2
|
+
export { toSnakeKeys } from './property-keys.js';
|
|
3
|
+
export { PostHogAnalyticsBoundary } from './posthog-analytics-boundary.js';
|
|
4
|
+
export { useAnalyticsPageView, } from './use-analytics-page-view.js';
|
|
5
|
+
export { usePostHogSiteRegistration, } from './use-posthog-site-registration.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Wires PostHog into the @curvenote/theme-ui AnalyticsBoundary so that
|
|
4
|
+
* useAnalytics() calls from packages/ui forward to the configured PostHog
|
|
5
|
+
* client. The supplied `context` (camelCase) is merged into every captured
|
|
6
|
+
* event with keys converted to snake_case.
|
|
7
|
+
*
|
|
8
|
+
* Uses the posthog-js singleton from initPosthogClient() (not usePostHog()) so
|
|
9
|
+
* capture stays wired even before the React provider settles.
|
|
10
|
+
*/
|
|
11
|
+
export declare function PostHogAnalyticsBoundary({ context, children, }: {
|
|
12
|
+
context?: Record<string, unknown>;
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
//# sourceMappingURL=posthog-analytics-boundary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"posthog-analytics-boundary.d.ts","sourceRoot":"","sources":["../src/posthog-analytics-boundary.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAIpD;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,EACvC,OAAO,EACP,QAAQ,GACT,EAAE;IACD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,QAAQ,EAAE,SAAS,CAAC;CACrB,2CASA"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback } from 'react';
|
|
3
|
+
import { AnalyticsBoundary } from '@curvenote/theme-ui';
|
|
4
|
+
import { capturePosthogBoundaryEvent } from './posthog-client.js';
|
|
5
|
+
/**
|
|
6
|
+
* Wires PostHog into the @curvenote/theme-ui AnalyticsBoundary so that
|
|
7
|
+
* useAnalytics() calls from packages/ui forward to the configured PostHog
|
|
8
|
+
* client. The supplied `context` (camelCase) is merged into every captured
|
|
9
|
+
* event with keys converted to snake_case.
|
|
10
|
+
*
|
|
11
|
+
* Uses the posthog-js singleton from initPosthogClient() (not usePostHog()) so
|
|
12
|
+
* capture stays wired even before the React provider settles.
|
|
13
|
+
*/
|
|
14
|
+
export function PostHogAnalyticsBoundary({ context, children, }) {
|
|
15
|
+
const capture = useCallback((event, properties) => {
|
|
16
|
+
capturePosthogBoundaryEvent(event, properties);
|
|
17
|
+
}, []);
|
|
18
|
+
return (_jsx(AnalyticsBoundary, { capture: capture, context: context, children: children }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import posthog from 'posthog-js';
|
|
2
|
+
export declare const POSTHOG_LOADED_EVENT = "posthog:loaded";
|
|
3
|
+
export declare function setPosthogClientEnabled(enabled: boolean): void;
|
|
4
|
+
export declare function isPosthogClientEnabled(): boolean;
|
|
5
|
+
/** Site fields registered as PostHog super-properties (`SiteDTO` and similar shapes satisfy this). */
|
|
6
|
+
export type PosthogSiteSuperProperties = {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
private?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export type InitPosthogClientOptions = {
|
|
12
|
+
projectToken: string;
|
|
13
|
+
apiHost: string;
|
|
14
|
+
/** Registered as a PostHog super-property on every event (e.g. `scms-theme`, `openrxiv-reader`). */
|
|
15
|
+
app: string;
|
|
16
|
+
/** When set, `site_id`, `site_name`, and `site_private` are registered at init (not after async load). */
|
|
17
|
+
site?: PosthogSiteSuperProperties | null;
|
|
18
|
+
};
|
|
19
|
+
export declare function siteToPosthogSuperProperties(site: PosthogSiteSuperProperties): Record<string, unknown>;
|
|
20
|
+
/** Register site super-properties on the PostHog client. No-op when analytics is disabled or `site.name` is missing. */
|
|
21
|
+
export declare function registerPosthogSiteSuperProperties(site: PosthogSiteSuperProperties | null | undefined): void;
|
|
22
|
+
/**
|
|
23
|
+
* Initialize the shared posthog-js singleton. Call once per page load from a root provider
|
|
24
|
+
* (e.g. with PostHog settings from a React Router loader).
|
|
25
|
+
*/
|
|
26
|
+
export declare function initPosthogClient({ projectToken, apiHost, app, site }: InitPosthogClientOptions): void;
|
|
27
|
+
/** PostHog capture with camelCase → snake_case (direct callers outside useAnalytics). */
|
|
28
|
+
export declare function capturePosthogClientEvent(event: string, properties?: Record<string, unknown>): void;
|
|
29
|
+
/** PostHog capture when properties are already snake_case (AnalyticsBoundary → useAnalytics). */
|
|
30
|
+
export declare function capturePosthogBoundaryEvent(event: string, properties?: Record<string, unknown>): void;
|
|
31
|
+
export { posthog };
|
|
32
|
+
//# sourceMappingURL=posthog-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"posthog-client.d.ts","sourceRoot":"","sources":["../src/posthog-client.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,YAAY,CAAC;AAGjC,eAAO,MAAM,oBAAoB,mBAAmB,CAAC;AAKrD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,QAEvD;AAED,wBAAgB,sBAAsB,IAAI,OAAO,CAEhD;AAED,sGAAsG;AACtG,MAAM,MAAM,0BAA0B,GAAG;IACvC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACrC,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,oGAAoG;IACpG,GAAG,EAAE,MAAM,CAAC;IACZ,0GAA0G;IAC1G,IAAI,CAAC,EAAE,0BAA0B,GAAG,IAAI,CAAC;CAC1C,CAAC;AAEF,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,0BAA0B,GAC/B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAMzB;AAED,wHAAwH;AACxH,wBAAgB,kCAAkC,CAChD,IAAI,EAAE,0BAA0B,GAAG,IAAI,GAAG,SAAS,GAClD,IAAI,CAGN;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,YAAY,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,wBAAwB,QAqB/F;AAED,yFAAyF;AACzF,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,MAAM,EACb,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,IAAI,CAGN;AAED,iGAAiG;AACjG,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,MAAM,EACb,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,IAAI,CAGN;AAED,OAAO,EAAE,OAAO,EAAE,CAAC"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import posthog from 'posthog-js';
|
|
2
|
+
import { toSnakeKeys } from './property-keys.js';
|
|
3
|
+
export const POSTHOG_LOADED_EVENT = 'posthog:loaded';
|
|
4
|
+
let clientEnabled = false;
|
|
5
|
+
let clientInitialized = false;
|
|
6
|
+
export function setPosthogClientEnabled(enabled) {
|
|
7
|
+
clientEnabled = enabled;
|
|
8
|
+
}
|
|
9
|
+
export function isPosthogClientEnabled() {
|
|
10
|
+
return clientEnabled;
|
|
11
|
+
}
|
|
12
|
+
export function siteToPosthogSuperProperties(site) {
|
|
13
|
+
return {
|
|
14
|
+
site_id: site.id,
|
|
15
|
+
site_name: site.name,
|
|
16
|
+
site_private: site.private === true,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Register site super-properties on the PostHog client. No-op when analytics is disabled or `site.name` is missing. */
|
|
20
|
+
export function registerPosthogSiteSuperProperties(site) {
|
|
21
|
+
if (!site?.name || !isPosthogClientEnabled())
|
|
22
|
+
return;
|
|
23
|
+
posthog.register(siteToPosthogSuperProperties(site));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Initialize the shared posthog-js singleton. Call once per page load from a root provider
|
|
27
|
+
* (e.g. with PostHog settings from a React Router loader).
|
|
28
|
+
*/
|
|
29
|
+
export function initPosthogClient({ projectToken, apiHost, app, site }) {
|
|
30
|
+
if (clientInitialized)
|
|
31
|
+
return;
|
|
32
|
+
const token = projectToken?.trim();
|
|
33
|
+
setPosthogClientEnabled(Boolean(token));
|
|
34
|
+
if (!token)
|
|
35
|
+
return;
|
|
36
|
+
clientInitialized = true;
|
|
37
|
+
posthog.init(token, {
|
|
38
|
+
api_host: apiHost?.trim() || 'https://us.i.posthog.com',
|
|
39
|
+
defaults: '2026-01-30',
|
|
40
|
+
__add_tracing_headers: [window.location.host],
|
|
41
|
+
loaded: () => {
|
|
42
|
+
window.dispatchEvent(new CustomEvent(POSTHOG_LOADED_EVENT));
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
posthog.register({
|
|
46
|
+
app,
|
|
47
|
+
...(site?.name ? siteToPosthogSuperProperties(site) : {}),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/** PostHog capture with camelCase → snake_case (direct callers outside useAnalytics). */
|
|
51
|
+
export function capturePosthogClientEvent(event, properties) {
|
|
52
|
+
if (!isPosthogClientEnabled())
|
|
53
|
+
return;
|
|
54
|
+
posthog.capture(event, toSnakeKeys(properties));
|
|
55
|
+
}
|
|
56
|
+
/** PostHog capture when properties are already snake_case (AnalyticsBoundary → useAnalytics). */
|
|
57
|
+
export function capturePosthogBoundaryEvent(event, properties) {
|
|
58
|
+
if (!isPosthogClientEnabled())
|
|
59
|
+
return;
|
|
60
|
+
posthog.capture(event, properties);
|
|
61
|
+
}
|
|
62
|
+
export { posthog };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function camelToSnake(s: string): string;
|
|
2
|
+
/** Convert analytics property keys from camelCase to PostHog snake_case. Skips undefined values. */
|
|
3
|
+
export declare function toSnakeKeys(input?: Record<string, unknown>): Record<string, unknown>;
|
|
4
|
+
//# sourceMappingURL=property-keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"property-keys.d.ts","sourceRoot":"","sources":["../src/property-keys.ts"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAK9C;AAED,oGAAoG;AACpG,wBAAgB,WAAW,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQpF"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function camelToSnake(s) {
|
|
2
|
+
return s
|
|
3
|
+
.replace(/([A-Z])/g, '_$1')
|
|
4
|
+
.replace(/^_/, '')
|
|
5
|
+
.toLowerCase();
|
|
6
|
+
}
|
|
7
|
+
/** Convert analytics property keys from camelCase to PostHog snake_case. Skips undefined values. */
|
|
8
|
+
export function toSnakeKeys(input) {
|
|
9
|
+
if (!input)
|
|
10
|
+
return {};
|
|
11
|
+
const out = {};
|
|
12
|
+
for (const [k, v] of Object.entries(input)) {
|
|
13
|
+
if (v === undefined)
|
|
14
|
+
continue;
|
|
15
|
+
out[camelToSnake(k)] = v;
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type UseAnalyticsPageViewOptions = {
|
|
2
|
+
/** When false, the event is not captured. Defaults to true. */
|
|
3
|
+
enabled?: boolean;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Captures a page-view-style analytics event once per navigation (location.key),
|
|
7
|
+
* after optional readiness gates pass. Dedupes React Strict Mode double-mounts.
|
|
8
|
+
*
|
|
9
|
+
* Does not wait on usePostHog().__loaded — initPosthogClient() runs before hydrate
|
|
10
|
+
* and posthog-js queues events until the client is ready.
|
|
11
|
+
*/
|
|
12
|
+
export declare function useAnalyticsPageView(event: string, properties?: Record<string, unknown>, deps?: readonly unknown[], options?: UseAnalyticsPageViewOptions): void;
|
|
13
|
+
//# sourceMappingURL=use-analytics-page-view.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-analytics-page-view.d.ts","sourceRoot":"","sources":["../src/use-analytics-page-view.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,2BAA2B,GAAG;IACxC,+DAA+D;IAC/D,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,MAAM,EACb,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,IAAI,GAAE,SAAS,OAAO,EAAO,EAC7B,OAAO,CAAC,EAAE,2BAA2B,QAgBtC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { useLocation } from 'react-router';
|
|
3
|
+
import { useAnalytics } from '@curvenote/theme-ui';
|
|
4
|
+
/**
|
|
5
|
+
* Captures a page-view-style analytics event once per navigation (location.key),
|
|
6
|
+
* after optional readiness gates pass. Dedupes React Strict Mode double-mounts.
|
|
7
|
+
*
|
|
8
|
+
* Does not wait on usePostHog().__loaded — initPosthogClient() runs before hydrate
|
|
9
|
+
* and posthog-js queues events until the client is ready.
|
|
10
|
+
*/
|
|
11
|
+
export function useAnalyticsPageView(event, properties, deps = [], options) {
|
|
12
|
+
const enabled = options?.enabled ?? true;
|
|
13
|
+
const capture = useAnalytics();
|
|
14
|
+
const location = useLocation();
|
|
15
|
+
const lastKeyRef = useRef(null);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!enabled)
|
|
18
|
+
return;
|
|
19
|
+
const dedupeKey = `${location.key}:${event}`;
|
|
20
|
+
if (lastKeyRef.current === dedupeKey)
|
|
21
|
+
return;
|
|
22
|
+
lastKeyRef.current = dedupeKey;
|
|
23
|
+
capture(event, properties);
|
|
24
|
+
}, [enabled, location.key, event, capture, ...deps]);
|
|
25
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type PosthogSiteSuperProperties } from './posthog-client.js';
|
|
2
|
+
/** Site fields read for PostHog super-properties (`SiteDTO` and similar shapes satisfy this). */
|
|
3
|
+
export type PostHogSiteRegistrationSite = PosthogSiteSuperProperties;
|
|
4
|
+
export type PostHogSiteRegistrationArgs = {
|
|
5
|
+
site: PostHogSiteRegistrationSite | null | undefined;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Keeps site super-properties (`site_id`, `site_name`, `site_private`) in sync when site
|
|
9
|
+
* loader data changes. Primary registration happens in `initPosthogClient()`; this hook covers
|
|
10
|
+
* late-arriving site data or updates after the first init.
|
|
11
|
+
*/
|
|
12
|
+
export declare function usePostHogSiteRegistration({ site }: PostHogSiteRegistrationArgs): void;
|
|
13
|
+
//# sourceMappingURL=use-posthog-site-registration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-posthog-site-registration.d.ts","sourceRoot":"","sources":["../src/use-posthog-site-registration.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,0BAA0B,EAChC,MAAM,qBAAqB,CAAC;AAE7B,iGAAiG;AACjG,MAAM,MAAM,2BAA2B,GAAG,0BAA0B,CAAC;AAErE,MAAM,MAAM,2BAA2B,GAAG;IACxC,IAAI,EAAE,2BAA2B,GAAG,IAAI,GAAG,SAAS,CAAC;CACtD,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,EAAE,IAAI,EAAE,EAAE,2BAA2B,QAI/E"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { registerPosthogSiteSuperProperties, } from './posthog-client.js';
|
|
3
|
+
/**
|
|
4
|
+
* Keeps site super-properties (`site_id`, `site_name`, `site_private`) in sync when site
|
|
5
|
+
* loader data changes. Primary registration happens in `initPosthogClient()`; this hook covers
|
|
6
|
+
* late-arriving site data or updates after the first init.
|
|
7
|
+
*/
|
|
8
|
+
export function usePostHogSiteRegistration({ site }) {
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
registerPosthogSiteSuperProperties(site);
|
|
11
|
+
}, [site?.id, site?.name, site?.private]);
|
|
12
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@curvenote/analytics-posthog",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"dev": "tsc --watch",
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"compile": "bun run typecheck",
|
|
23
|
+
"lint": "eslint \"src/**/*.ts*\" -c ./.eslintrc.cjs",
|
|
24
|
+
"lint:format": "prettier --check \"src/**/*.{ts,tsx,md}\"",
|
|
25
|
+
"clean": "rm -rf dist",
|
|
26
|
+
"prepublishOnly": "bun run build"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"posthog-js": "^1.372.8"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@curvenote/theme-ui": "^2.0.0",
|
|
33
|
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0",
|
|
34
|
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0",
|
|
35
|
+
"react-router": "^7.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@curvenote/theme-ui": "workspace:*",
|
|
39
|
+
"react-router": "^7.5.3"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
}
|
|
44
|
+
}
|