@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 ADDED
@@ -0,0 +1,178 @@
1
+ # @curvenote/analytics-posthog
2
+
3
+ [![@curvenote/analytics-posthog on npm](https://img.shields.io/npm/v/@curvenote/analytics-posthog.svg)](https://www.npmjs.com/package/@curvenote/analytics-posthog)
4
+ [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](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
@@ -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
+ }