@activescott/analytics 0.2.0 → 0.3.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 CHANGED
@@ -20,34 +20,52 @@ release a matching minor of this package.
20
20
 
21
21
  Mount it where loader data is available (in React Router v7, that means `App`,
22
22
  not `Layout` — `Layout` renders with undefined loader data on error
23
- boundaries, and error pages intentionally get no pageview):
23
+ boundaries, and error pages intentionally get no pageview).
24
+
25
+ For zero bundle cost when disabled, use `LazyPostHogProvider`: it renders
26
+ null unless enabled with a key, otherwise suspense-loads the initialized
27
+ tree with posthog-js in its own chunk. Mount it as a STABLE SIBLING of your
28
+ UI — never as a conditional wrapper around it, which would remount the whole
29
+ subtree once the chunk loads:
24
30
 
25
31
  ```tsx
26
32
  // app/root.tsx
27
- import { PostHogProvider } from "@activescott/analytics"
33
+ import { LazyPostHogProvider } from "@activescott/analytics"
28
34
 
29
35
  export function App() {
30
- const { posthogKey, user } = useLoaderData<typeof loader>()
31
- // Dynamic-import the provider module only when a key is configured so
32
- // disabled/self-host installs pay zero bundle cost for posthog-js.
33
- ...
36
+ const { posthogKey, isAdmin, user } = useLoaderData<typeof loader>()
37
+ return (
38
+ <>
39
+ <LazyPostHogProvider
40
+ enabled={posthogKey !== "" && !isAdmin}
41
+ apiKey={posthogKey || undefined}
42
+ options={{ api_host: "/ph", ui_host: "https://us.posthog.com" }}
43
+ user={user ? { distinctId: user.id } : undefined}
44
+ />
45
+ <Outlet />
46
+ </>
47
+ )
34
48
  }
35
49
  ```
36
50
 
37
51
  Identify by stable id only — never pass emails, phone numbers, or other PII in
38
- `properties` unless your privacy policy explicitly covers it:
52
+ `properties` unless your privacy policy explicitly covers it.
53
+
54
+ `PostHogProvider` (same props plus `children`) remains for apps that prefer
55
+ a static import and a wrapping provider.
56
+
57
+ ### Custom events
58
+
59
+ Capture through `captureAnalyticsEvent` or the `useAnalyticsCapture` hook —
60
+ both read the client the provider registers on init, so they work anywhere
61
+ without a provider ancestor, and drop while disabled or still loading:
39
62
 
40
63
  ```tsx
41
- <PostHogProvider
42
- apiKey={posthogKey}
43
- options={{ api_host: "/ph", ui_host: "https://us.posthog.com" }}
44
- user={user ? { distinctId: user.id } : undefined}
45
- >
46
- ...
47
- </PostHogProvider>
48
- ```
64
+ import { useAnalyticsCapture } from "@activescott/analytics"
49
65
 
50
- With no `apiKey` the provider renders children with zero PostHog code paths.
66
+ const capture = useAnalyticsCapture()
67
+ capture("search_results_shown", { result_count: 2 })
68
+ ```
51
69
 
52
70
  ### 2. Add the reverse proxy route
53
71
 
@@ -86,6 +104,14 @@ That's it! PostHog will now:
86
104
  - Identify logged-in users by id
87
105
  - Receive events through your own domain (`/ph/*`)
88
106
 
107
+ ### Deriving sibling origins
108
+
109
+ `@activescott/analytics/hosts` exports the pure helpers `posthogAssetsHost`
110
+ and `posthogUiHost`, which map a PostHog API origin to its assets and app-UI
111
+ siblings (`<region>.i.posthog.com` → `<region>-assets.i.posthog.com` /
112
+ `<region>.posthog.com`; anything else passes through unchanged) — so a
113
+ deployment carries one host knob instead of three.
114
+
89
115
  ## Versioning
90
116
 
91
117
  Independent plain-npm versions with git tags of the form `analytics@<version>`
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Registry bridging the initialized posthog-js client to code that cannot
3
+ * (or should not) sit under the provider's React context.
4
+ *
5
+ * LazyPostHogProvider mounts childless as a stable sibling of the app UI —
6
+ * conditionally wrapping UI would remount the subtree when the chunk loads —
7
+ * so custom events cannot reach the client through context. Instead the
8
+ * registrar inside the initialized tree publishes the bound capture here on
9
+ * mount (and clears it on unmount); captureAnalyticsEvent and
10
+ * useAnalyticsCapture read it back. Unregistered (disabled, or not yet
11
+ * loaded) captures are dropped, never queued: analytics must not delay
12
+ * interaction.
13
+ */
14
+ export type AnalyticsCapture = (event: string, properties?: Record<string, unknown>) => void;
15
+ /**
16
+ * Publishes (or clears, with null) the bound capture function. Called by the
17
+ * registrar inside the initialized provider tree; package-internal.
18
+ */
19
+ export declare function setCaptureClient(implementation: AnalyticsCapture | null): void;
20
+ /**
21
+ * Fire-and-forget event capture. Drops when analytics is disabled or the
22
+ * client has not initialized yet, and never throws: a capture failure must
23
+ * not break the UI.
24
+ */
25
+ export declare function captureAnalyticsEvent(event: string, properties?: Record<string, unknown>): void;
26
+ //# sourceMappingURL=capture-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture-client.d.ts","sourceRoot":"","sources":["../src/capture-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,MAAM,gBAAgB,GAAG,CAC7B,KAAK,EAAE,MAAM,EACb,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KACjC,IAAI,CAAA;AAIT;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,cAAc,EAAE,gBAAgB,GAAG,IAAI,GACtC,IAAI,CAEN;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,MAAM,EACb,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,IAAI,CAMN"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Registry bridging the initialized posthog-js client to code that cannot
3
+ * (or should not) sit under the provider's React context.
4
+ *
5
+ * LazyPostHogProvider mounts childless as a stable sibling of the app UI —
6
+ * conditionally wrapping UI would remount the subtree when the chunk loads —
7
+ * so custom events cannot reach the client through context. Instead the
8
+ * registrar inside the initialized tree publishes the bound capture here on
9
+ * mount (and clears it on unmount); captureAnalyticsEvent and
10
+ * useAnalyticsCapture read it back. Unregistered (disabled, or not yet
11
+ * loaded) captures are dropped, never queued: analytics must not delay
12
+ * interaction.
13
+ */
14
+ let captureImplementation = null;
15
+ /**
16
+ * Publishes (or clears, with null) the bound capture function. Called by the
17
+ * registrar inside the initialized provider tree; package-internal.
18
+ */
19
+ export function setCaptureClient(implementation) {
20
+ captureImplementation = implementation;
21
+ }
22
+ /**
23
+ * Fire-and-forget event capture. Drops when analytics is disabled or the
24
+ * client has not initialized yet, and never throws: a capture failure must
25
+ * not break the UI.
26
+ */
27
+ export function captureAnalyticsEvent(event, properties) {
28
+ try {
29
+ captureImplementation?.(event, properties);
30
+ }
31
+ catch {
32
+ // Dropped by design.
33
+ }
34
+ }
35
+ //# sourceMappingURL=capture-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture-client.js","sourceRoot":"","sources":["../src/capture-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAOH,IAAI,qBAAqB,GAA4B,IAAI,CAAA;AAEzD;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAC9B,cAAuC;IAEvC,qBAAqB,GAAG,cAAc,CAAA;AACxC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CACnC,KAAa,EACb,UAAoC;IAEpC,IAAI,CAAC;QACH,qBAAqB,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,CAAA;IAC5C,CAAC;IAAC,MAAM,CAAC;QACP,qBAAqB;IACvB,CAAC;AACH,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Publishes the initialized client's bound capture into the module registry
3
+ * (and clears it on unmount) so captureAnalyticsEvent / useAnalyticsCapture
4
+ * work without a provider ancestor. Renders nothing; mount once inside the
5
+ * initialized provider tree.
6
+ */
7
+ export declare function CaptureRegistrar(): null;
8
+ //# sourceMappingURL=capture-registrar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture-registrar.d.ts","sourceRoot":"","sources":["../src/capture-registrar.tsx"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,wBAAgB,gBAAgB,SAa/B"}
@@ -0,0 +1,20 @@
1
+ import { useEffect } from "react";
2
+ import { usePostHog } from "posthog-js/react";
3
+ import { setCaptureClient } from "./capture-client.js";
4
+ /**
5
+ * Publishes the initialized client's bound capture into the module registry
6
+ * (and clears it on unmount) so captureAnalyticsEvent / useAnalyticsCapture
7
+ * work without a provider ancestor. Renders nothing; mount once inside the
8
+ * initialized provider tree.
9
+ */
10
+ export function CaptureRegistrar() {
11
+ const posthog = usePostHog();
12
+ useEffect(() => {
13
+ setCaptureClient((event, properties) => posthog.capture(event, properties));
14
+ return () => {
15
+ setCaptureClient(null);
16
+ };
17
+ }, [posthog]);
18
+ return null;
19
+ }
20
+ //# sourceMappingURL=capture-registrar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture-registrar.js","sourceRoot":"","sources":["../src/capture-registrar.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACjC,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAEtD;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB;IAC9B,MAAM,OAAO,GAAG,UAAU,EAAE,CAAA;IAE5B,SAAS,CAAC,GAAG,EAAE;QACb,gBAAgB,CAAC,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,CACrC,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,UAAU,CAAC,CACnC,CAAA;QACD,OAAO,GAAG,EAAE;YACV,gBAAgB,CAAC,IAAI,CAAC,CAAA;QACxB,CAAC,CAAA;IACH,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAA;IAEb,OAAO,IAAI,CAAA;AACb,CAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Derives the sibling PostHog origins from the configured API origin, so a
3
+ * deployment carries one host knob instead of three.
4
+ *
5
+ * PostHog cloud serves each region from `<region>.i.posthog.com` (events),
6
+ * `<region>-assets.i.posthog.com` (static assets) and `<region>.posthog.com`
7
+ * (app UI, the posthog-js `ui_host`). Any other origin — a self-hosted
8
+ * instance, a future region layout — is returned unchanged for both, which
9
+ * keeps proxy and client on the one configured host.
10
+ */
11
+ /**
12
+ * Returns the static-assets origin for a PostHog API origin.
13
+ */
14
+ export declare function posthogAssetsHost(apiHost: string): string;
15
+ /**
16
+ * Returns the app-UI origin for a PostHog API origin (the `ui_host`
17
+ * posthog-js option).
18
+ */
19
+ export declare function posthogUiHost(apiHost: string): string;
20
+ //# sourceMappingURL=hosts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hosts.d.ts","sourceRoot":"","sources":["../src/hosts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGzD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGrD"}
package/dist/hosts.js ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Derives the sibling PostHog origins from the configured API origin, so a
3
+ * deployment carries one host knob instead of three.
4
+ *
5
+ * PostHog cloud serves each region from `<region>.i.posthog.com` (events),
6
+ * `<region>-assets.i.posthog.com` (static assets) and `<region>.posthog.com`
7
+ * (app UI, the posthog-js `ui_host`). Any other origin — a self-hosted
8
+ * instance, a future region layout — is returned unchanged for both, which
9
+ * keeps proxy and client on the one configured host.
10
+ */
11
+ /**
12
+ * Returns the static-assets origin for a PostHog API origin.
13
+ */
14
+ export function posthogAssetsHost(apiHost) {
15
+ const region = posthogCloudRegion(apiHost);
16
+ return region ? `https://${region}-assets.i.posthog.com` : apiHost;
17
+ }
18
+ /**
19
+ * Returns the app-UI origin for a PostHog API origin (the `ui_host`
20
+ * posthog-js option).
21
+ */
22
+ export function posthogUiHost(apiHost) {
23
+ const region = posthogCloudRegion(apiHost);
24
+ return region ? `https://${region}.posthog.com` : apiHost;
25
+ }
26
+ function posthogCloudRegion(apiHost) {
27
+ let hostname;
28
+ try {
29
+ hostname = new URL(apiHost).hostname;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ const match = /^([a-z0-9-]+)\.i\.posthog\.com$/.exec(hostname);
35
+ return match ? match[1] : null;
36
+ }
37
+ //# sourceMappingURL=hosts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hosts.js","sourceRoot":"","sources":["../src/hosts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAe;IAC/C,MAAM,MAAM,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAA;IAC1C,OAAO,MAAM,CAAC,CAAC,CAAC,WAAW,MAAM,uBAAuB,CAAC,CAAC,CAAC,OAAO,CAAA;AACpE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,OAAe;IAC3C,MAAM,MAAM,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAA;IAC1C,OAAO,MAAM,CAAC,CAAC,CAAC,WAAW,MAAM,cAAc,CAAC,CAAC,CAAC,OAAO,CAAA;AAC3D,CAAC;AAED,SAAS,kBAAkB,CAAC,OAAe;IACzC,IAAI,QAAgB,CAAA;IACpB,IAAI,CAAC;QACH,QAAQ,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAA;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;IACD,MAAM,KAAK,GAAG,iCAAiC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IAC9D,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;AAChC,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  export { PostHogProvider } from "./post-hog-provider.js";
2
2
  export { PostHogPageviewTracker } from "./post-hog-pageview-tracker.js";
3
3
  export { PostHogIdentifier } from "./post-hog-identifier.js";
4
+ export { LazyPostHogProvider, type LazyPostHogProviderProps, } from "./post-hog-lazy-provider.js";
5
+ export { captureAnalyticsEvent, type AnalyticsCapture, } from "./capture-client.js";
6
+ export { useAnalyticsCapture } from "./use-analytics-capture.js";
4
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAA;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAA;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AAC5D,OAAO,EACL,mBAAmB,EACnB,KAAK,wBAAwB,GAC9B,MAAM,6BAA6B,CAAA;AACpC,OAAO,EACL,qBAAqB,EACrB,KAAK,gBAAgB,GACtB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA"}
package/dist/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  export { PostHogProvider } from "./post-hog-provider.js";
2
2
  export { PostHogPageviewTracker } from "./post-hog-pageview-tracker.js";
3
3
  export { PostHogIdentifier } from "./post-hog-identifier.js";
4
+ export { LazyPostHogProvider, } from "./post-hog-lazy-provider.js";
5
+ export { captureAnalyticsEvent, } from "./capture-client.js";
6
+ export { useAnalyticsCapture } from "./use-analytics-capture.js";
4
7
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAA;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAA;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AAC5D,OAAO,EACL,mBAAmB,GAEpB,MAAM,6BAA6B,CAAA;AACpC,OAAO,EACL,qBAAqB,GAEtB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA"}
@@ -0,0 +1,19 @@
1
+ import type { PostHogConfig } from "posthog-js";
2
+ interface PostHogInitProps {
3
+ apiKey: string;
4
+ options?: Partial<PostHogConfig>;
5
+ user?: {
6
+ distinctId: string;
7
+ properties?: Record<string, unknown>;
8
+ };
9
+ children?: React.ReactNode;
10
+ }
11
+ /**
12
+ * The initialized PostHog tree: context provider, SPA pageview tracking,
13
+ * user identification, and capture registration. Lives in its own module so
14
+ * React.lazy splits it (and posthog-js with it) into a separate chunk —
15
+ * mount LazyPostHogProvider, not this, which guarantees the split point.
16
+ */
17
+ export declare function PostHogInit({ apiKey, options, user, children, }: PostHogInitProps): import("react").JSX.Element;
18
+ export {};
19
+ //# sourceMappingURL=post-hog-init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"post-hog-init.d.ts","sourceRoot":"","sources":["../src/post-hog-init.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAM/C,UAAU,gBAAgB;IACxB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAA;IAChC,IAAI,CAAC,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAA;IACnE,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC3B;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EACN,OAAO,EACP,IAAI,EACJ,QAAQ,GACT,EAAE,gBAAgB,+BAkBlB"}
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PostHogProvider as PHProvider } from "posthog-js/react";
3
+ import { PostHogIdentifier } from "./post-hog-identifier.js";
4
+ import { PostHogPageviewTracker } from "./post-hog-pageview-tracker.js";
5
+ import { CaptureRegistrar } from "./capture-registrar.js";
6
+ /**
7
+ * The initialized PostHog tree: context provider, SPA pageview tracking,
8
+ * user identification, and capture registration. Lives in its own module so
9
+ * React.lazy splits it (and posthog-js with it) into a separate chunk —
10
+ * mount LazyPostHogProvider, not this, which guarantees the split point.
11
+ */
12
+ export function PostHogInit({ apiKey, options, user, children, }) {
13
+ return (_jsxs(PHProvider, { apiKey: apiKey, options: {
14
+ ...options,
15
+ capture_pageview: false,
16
+ }, children: [_jsx(PostHogPageviewTracker, {}), _jsx(PostHogIdentifier, { distinctId: user?.distinctId, properties: user?.properties }), _jsx(CaptureRegistrar, {}), children] }));
17
+ }
18
+ //# sourceMappingURL=post-hog-init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"post-hog-init.js","sourceRoot":"","sources":["../src/post-hog-init.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,eAAe,IAAI,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AAC5D,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAA;AACvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AASzD;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,EAC1B,MAAM,EACN,OAAO,EACP,IAAI,EACJ,QAAQ,GACS;IACjB,OAAO,CACL,MAAC,UAAU,IACT,MAAM,EAAE,MAAM,EACd,OAAO,EAAE;YACP,GAAG,OAAO;YACV,gBAAgB,EAAE,KAAK;SACxB,aAED,KAAC,sBAAsB,KAAG,EAC1B,KAAC,iBAAiB,IAChB,UAAU,EAAE,IAAI,EAAE,UAAU,EAC5B,UAAU,EAAE,IAAI,EAAE,UAAU,GAC5B,EACF,KAAC,gBAAgB,KAAG,EACnB,QAAQ,IACE,CACd,CAAA;AACH,CAAC"}
@@ -0,0 +1,29 @@
1
+ import type { PostHogConfig } from "posthog-js";
2
+ export interface LazyPostHogProviderProps {
3
+ /**
4
+ * Whether capture runs at all. The containing app decides — typically
5
+ * `apiKey` presence plus its own exclusions (e.g. admin traffic). When
6
+ * false (or with no key) this renders null with zero PostHog code paths,
7
+ * and the posthog-js chunk is never fetched.
8
+ */
9
+ enabled: boolean;
10
+ apiKey?: string;
11
+ options?: Partial<PostHogConfig>;
12
+ user?: {
13
+ distinctId: string;
14
+ properties?: Record<string, unknown>;
15
+ };
16
+ }
17
+ /**
18
+ * PostHog with zero bundle cost when disabled. Renders null unless enabled
19
+ * with a key; otherwise suspense-loads the initialized tree — posthog-js
20
+ * travels in its own chunk, fetched only then.
21
+ *
22
+ * Mount as a STABLE SIBLING of your UI (e.g. beside `<Outlet/>`), never as a
23
+ * conditional wrapper around it: swapping a wrapper in after the chunk loads
24
+ * remounts the whole subtree. Custom events go through captureAnalyticsEvent
25
+ * / useAnalyticsCapture (registered on init), so no context ancestor is
26
+ * needed.
27
+ */
28
+ export declare function LazyPostHogProvider({ enabled, apiKey, options, user, }: LazyPostHogProviderProps): import("react").JSX.Element | null;
29
+ //# sourceMappingURL=post-hog-lazy-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"post-hog-lazy-provider.d.ts","sourceRoot":"","sources":["../src/post-hog-lazy-provider.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAQ/C,MAAM,WAAW,wBAAwB;IACvC;;;;;OAKG;IACH,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAA;IAChC,IAAI,CAAC,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAA;CACpE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,OAAO,EACP,MAAM,EACN,OAAO,EACP,IAAI,GACL,EAAE,wBAAwB,sCAS1B"}
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Suspense, lazy } from "react";
3
+ const PostHogInit = lazy(() => import("./post-hog-init.js").then((module) => ({
4
+ default: module.PostHogInit,
5
+ })));
6
+ /**
7
+ * PostHog with zero bundle cost when disabled. Renders null unless enabled
8
+ * with a key; otherwise suspense-loads the initialized tree — posthog-js
9
+ * travels in its own chunk, fetched only then.
10
+ *
11
+ * Mount as a STABLE SIBLING of your UI (e.g. beside `<Outlet/>`), never as a
12
+ * conditional wrapper around it: swapping a wrapper in after the chunk loads
13
+ * remounts the whole subtree. Custom events go through captureAnalyticsEvent
14
+ * / useAnalyticsCapture (registered on init), so no context ancestor is
15
+ * needed.
16
+ */
17
+ export function LazyPostHogProvider({ enabled, apiKey, options, user, }) {
18
+ if (!enabled || !apiKey) {
19
+ return null;
20
+ }
21
+ return (_jsx(Suspense, { fallback: null, children: _jsx(PostHogInit, { apiKey: apiKey, options: options, user: user }) }));
22
+ }
23
+ //# sourceMappingURL=post-hog-lazy-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"post-hog-lazy-provider.js","sourceRoot":"","sources":["../src/post-hog-lazy-provider.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;AAGtC,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,EAAE,CAC5B,MAAM,CAAC,oBAAoB,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC7C,OAAO,EAAE,MAAM,CAAC,WAAW;CAC5B,CAAC,CAAC,CACJ,CAAA;AAeD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,mBAAmB,CAAC,EAClC,OAAO,EACP,MAAM,EACN,OAAO,EACP,IAAI,GACqB;IACzB,IAAI,CAAC,OAAO,IAAI,CAAC,MAAM,EAAE,CAAC;QACxB,OAAO,IAAI,CAAA;IACb,CAAC;IACD,OAAO,CACL,KAAC,QAAQ,IAAC,QAAQ,EAAE,IAAI,YACtB,KAAC,WAAW,IAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,GAAI,GACpD,CACZ,CAAA;AACH,CAAC"}
@@ -6,7 +6,7 @@ interface PostHogProviderProps {
6
6
  distinctId: string;
7
7
  properties?: Record<string, unknown>;
8
8
  };
9
- children: React.ReactNode;
9
+ children?: React.ReactNode;
10
10
  }
11
11
  /**
12
12
  * All-in-one PostHog provider for React Router apps.
@@ -1 +1 @@
1
- {"version":3,"file":"post-hog-provider.d.ts","sourceRoot":"","sources":["../src/post-hog-provider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAK/C,UAAU,oBAAoB;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAA;IAChC,IAAI,CAAC,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAA;IACnE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAC1B;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,EAC9B,MAAM,EACN,OAAO,EACP,IAAI,EACJ,QAAQ,GACT,EAAE,oBAAoB,+BAqBtB"}
1
+ {"version":3,"file":"post-hog-provider.d.ts","sourceRoot":"","sources":["../src/post-hog-provider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAK/C,UAAU,oBAAoB;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAA;IAChC,IAAI,CAAC,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAA;IAKnE,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC3B;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,EAC9B,MAAM,EACN,OAAO,EACP,IAAI,EACJ,QAAQ,GACT,EAAE,oBAAoB,+BAqBtB"}
@@ -1 +1 @@
1
- {"version":3,"file":"post-hog-provider.js","sourceRoot":"","sources":["../src/post-hog-provider.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,eAAe,IAAI,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAChE,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAA;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AAS5D;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,EAC9B,MAAM,EACN,OAAO,EACP,IAAI,EACJ,QAAQ,GACa;IACrB,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,4BAAG,QAAQ,GAAI,CAAA;IACxB,CAAC;IAED,OAAO,CACL,MAAC,UAAU,IACT,MAAM,EAAE,MAAM,EACd,OAAO,EAAE;YACP,GAAG,OAAO;YACV,gBAAgB,EAAE,KAAK;SACxB,aAED,KAAC,sBAAsB,KAAG,EAC1B,KAAC,iBAAiB,IAChB,UAAU,EAAE,IAAI,EAAE,UAAU,EAC5B,UAAU,EAAE,IAAI,EAAE,UAAU,GAC5B,EACD,QAAQ,IACE,CACd,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"post-hog-provider.js","sourceRoot":"","sources":["../src/post-hog-provider.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,eAAe,IAAI,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAChE,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAA;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AAa5D;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,EAC9B,MAAM,EACN,OAAO,EACP,IAAI,EACJ,QAAQ,GACa;IACrB,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,4BAAG,QAAQ,GAAI,CAAA;IACxB,CAAC;IAED,OAAO,CACL,MAAC,UAAU,IACT,MAAM,EAAE,MAAM,EACd,OAAO,EAAE;YACP,GAAG,OAAO;YACV,gBAAgB,EAAE,KAAK;SACxB,aAED,KAAC,sBAAsB,KAAG,EAC1B,KAAC,iBAAiB,IAChB,UAAU,EAAE,IAAI,EAAE,UAAU,EAC5B,UAAU,EAAE,IAAI,EAAE,UAAU,GAC5B,EACD,QAAQ,IACE,CACd,CAAA;AACH,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { type AnalyticsCapture } from "./capture-client.js";
2
+ /**
3
+ * Returns the analytics capture function. Stable across renders; backed by
4
+ * the initialized provider's registered client, so it works anywhere in the
5
+ * app without a provider ancestor. Drops events while analytics is disabled
6
+ * or the client chunk is still loading.
7
+ */
8
+ export declare function useAnalyticsCapture(): AnalyticsCapture;
9
+ //# sourceMappingURL=use-analytics-capture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-analytics-capture.d.ts","sourceRoot":"","sources":["../src/use-analytics-capture.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,qBAAqB,CAAA;AAE5B;;;;;GAKG;AACH,wBAAgB,mBAAmB,IAAI,gBAAgB,CAItD"}
@@ -0,0 +1,14 @@
1
+ import { useCallback } from "react";
2
+ import { captureAnalyticsEvent, } from "./capture-client.js";
3
+ /**
4
+ * Returns the analytics capture function. Stable across renders; backed by
5
+ * the initialized provider's registered client, so it works anywhere in the
6
+ * app without a provider ancestor. Drops events while analytics is disabled
7
+ * or the client chunk is still loading.
8
+ */
9
+ export function useAnalyticsCapture() {
10
+ return useCallback((event, properties) => {
11
+ captureAnalyticsEvent(event, properties);
12
+ }, []);
13
+ }
14
+ //# sourceMappingURL=use-analytics-capture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-analytics-capture.js","sourceRoot":"","sources":["../src/use-analytics-capture.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AACnC,OAAO,EACL,qBAAqB,GAEtB,MAAM,qBAAqB,CAAA;AAE5B;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB;IACjC,OAAO,WAAW,CAAC,CAAC,KAAa,EAAE,UAAoC,EAAE,EAAE;QACzE,qBAAqB,CAAC,KAAK,EAAE,UAAU,CAAC,CAAA;IAC1C,CAAC,EAAE,EAAE,CAAC,CAAA;AACR,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@activescott/analytics",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "PostHog analytics integration for React Router apps — provider, SPA pageview tracking, user identification, and reverse proxy helper",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -13,6 +13,10 @@
13
13
  "./proxy": {
14
14
  "types": "./dist/proxy.d.ts",
15
15
  "import": "./dist/proxy.js"
16
+ },
17
+ "./hosts": {
18
+ "types": "./dist/hosts.d.ts",
19
+ "import": "./dist/hosts.js"
16
20
  }
17
21
  },
18
22
  "files": [
@@ -35,7 +39,7 @@
35
39
  "license": "MIT",
36
40
  "repository": {
37
41
  "type": "git",
38
- "url": "https://github.com/activescott/js-utils.git",
42
+ "url": "git+https://github.com/activescott/js-utils.git",
39
43
  "directory": "packages/analytics"
40
44
  },
41
45
  "peerDependencies": {
@@ -44,10 +48,14 @@
44
48
  "react-router": "^7.0.0"
45
49
  },
46
50
  "devDependencies": {
51
+ "@testing-library/dom": "^10.4.1",
52
+ "@testing-library/react": "^16.3.2",
47
53
  "@types/node": "^22",
48
54
  "@types/react": "^19.2.14",
55
+ "jsdom": "^27.4.0",
49
56
  "posthog-js": "^1.351.4",
50
- "react": "^19.2.4",
57
+ "react": "19.2.8",
58
+ "react-dom": "19.2.8",
51
59
  "react-router": "^7.10.1",
52
60
  "typescript": "^5.9.3",
53
61
  "vitest": "4.1.10"