@refraction-ui/react 0.6.0 → 0.8.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.
@@ -0,0 +1,219 @@
1
+ import { AnalyticsSink, AnalyticsEvent } from '../analytics/index.js';
2
+
3
+ /**
4
+ * @refraction-ui/analytics-sink-ga4 — option contracts.
5
+ *
6
+ * GA4 is "just a sink" in the epic's model (no privileged engine). This
7
+ * adapter implements the `AnalyticsSink` SPI from `@refraction-ui/analytics`
8
+ * in two interchangeable modes:
9
+ *
10
+ * - `client-sdk` — lazy-loads gtag.js in the browser (no hard vendor dep;
11
+ * the script is injected on first delivery only). Bridges Consent Mode.
12
+ * - `http` — GA4 Measurement Protocol (`/mp/collect`). No browser
13
+ * library is ever loaded — server-relay friendly.
14
+ *
15
+ * Default = `http` (protocol adapter, no vendor lib in the browser), matching
16
+ * the epic's "default = protocol adapters" stance.
17
+ */
18
+ /** Minimal gtag.js function signature (we never import the real types). */
19
+ type GtagFn = (...args: unknown[]) => void;
20
+ /** GA4 Consent Mode signal values. */
21
+ type ConsentState = 'granted' | 'denied';
22
+ /**
23
+ * GA4 Consent Mode bridge — maps our consent categories to the gtag
24
+ * `consent` command. Only used in `client-sdk` mode.
25
+ */
26
+ interface GA4ConsentBridge {
27
+ /**
28
+ * Default consent state pushed via `gtag('consent', 'default', …)` before
29
+ * the GA4 tag loads. Keys are GA4 consent types
30
+ * (`analytics_storage`, `ad_storage`, `ad_user_data`,
31
+ * `ad_personalization`, `functionality_storage`, …).
32
+ */
33
+ default?: Record<string, ConsentState>;
34
+ /**
35
+ * Maps a refraction consent category → the GA4 consent types it controls.
36
+ * When the router reports the sink may deliver (category granted) the
37
+ * adapter pushes `gtag('consent', 'update', { <types>: 'granted' })`.
38
+ * Example: `{ analytics: ['analytics_storage'] }`.
39
+ */
40
+ map?: Record<string, string[]>;
41
+ }
42
+ interface GA4CommonOptions {
43
+ /** GA4 Measurement ID, e.g. `G-XXXXXXXXXX`. */
44
+ measurementId: string;
45
+ /**
46
+ * Consent categories this sink requires. The router will not deliver until
47
+ * all are granted. Default: `['analytics']`.
48
+ */
49
+ consentCategories?: string[];
50
+ /** Stable sink name. Default `'ga4'`. */
51
+ name?: string;
52
+ }
53
+ /** `client-sdk` mode — runs gtag.js in the browser. */
54
+ interface GA4ClientSdkOptions extends GA4CommonOptions {
55
+ mode: 'client-sdk';
56
+ /**
57
+ * Inject the GA4 Consent Mode bridge. The default state (if any) is pushed
58
+ * before the tag loads; category grants drive `consent: update`.
59
+ */
60
+ consentMode?: GA4ConsentBridge;
61
+ /**
62
+ * Inject an existing gtag function (tests / apps that manage the tag
63
+ * themselves). When provided, the script loader is NOT used and **no**
64
+ * vendor script is injected.
65
+ */
66
+ gtag?: GtagFn;
67
+ /**
68
+ * Inject the script loader (tests / SSR-safe apps). Receives the gtag.js
69
+ * src URL; must resolve once the script has executed. Defaults to a DOM
70
+ * `<script>` injector (browser only).
71
+ */
72
+ scriptLoader?: (src: string) => Promise<void>;
73
+ /** Override the gtag.js base URL (testing). */
74
+ gtagSrcBase?: string;
75
+ }
76
+ /** `http` mode — GA4 Measurement Protocol. No browser library. */
77
+ interface GA4HttpOptions extends GA4CommonOptions {
78
+ mode?: 'http';
79
+ /** GA4 Measurement Protocol API secret (server-side credential). */
80
+ apiSecret: string;
81
+ /** Override the `/mp/collect` base URL (testing / EU endpoint). */
82
+ endpoint?: string;
83
+ /** Use the Measurement Protocol `/debug/mp/collect` validation endpoint. */
84
+ debug?: boolean;
85
+ /** Injected fetch (defaults to global fetch). Never loads a vendor lib. */
86
+ fetchImpl?: typeof fetch;
87
+ }
88
+ /** Discriminated union of the two modes. */
89
+ type GA4SinkOptions = GA4ClientSdkOptions | GA4HttpOptions;
90
+
91
+ /**
92
+ * `createGA4Sink` — the single entry point. Dispatches on `mode`:
93
+ * - `mode: 'client-sdk'` → lazy gtag.js adapter
94
+ * - `mode: 'http'` (default) → Measurement Protocol adapter, no vendor lib
95
+ *
96
+ * Default is `http` to honour the epic's "default = protocol adapters (no
97
+ * vendor libs in the browser)" stance. GA4 is just a sink — register it via
98
+ * `createAnalytics({ sinks: [createGA4Sink(...)] })` or `analytics.addSink`.
99
+ *
100
+ * Both factories are independent modules. The `http` factory has ZERO
101
+ * references to gtag.js / DOM-script code, and the `client-sdk` factory only
102
+ * touches the vendor script lazily (first delivery). Selecting one mode never
103
+ * executes the other's load path; consumers that only ever construct an
104
+ * `http` sink never run any vendor code.
105
+ */
106
+
107
+ declare function createGA4Sink(options: GA4SinkOptions): AnalyticsSink;
108
+
109
+ /**
110
+ * GA4 `http` adapter — Measurement Protocol (`/mp/collect`).
111
+ *
112
+ * Server-relay friendly: this path NEVER loads gtag.js or any browser
113
+ * library. It only POSTs JSON to the Measurement Protocol endpoint:
114
+ *
115
+ * POST {endpoint}/mp/collect?measurement_id={id}&api_secret={secret}
116
+ * Content-Type: application/json
117
+ * { client_id, user_id?, user_properties?, events: [{ name, params }] }
118
+ *
119
+ * GA4 Measurement Protocol constraints honoured here:
120
+ * - up to 25 events per request → batches are chunked.
121
+ * - `identify` envelopes carry no event; they still POST so user_id /
122
+ * user_properties propagate (events array may be empty for a user-props
123
+ * only ping, which GA4 accepts).
124
+ * - 2xx (incl. 204) = accepted. The MP endpoint always returns 2xx for
125
+ * well-formed requests; the `debug` endpoint returns validation messages.
126
+ */
127
+
128
+ /**
129
+ * Create the GA4 Measurement-Protocol (`http`) sink.
130
+ *
131
+ * No vendor library is loaded on this path under any circumstance.
132
+ */
133
+ declare function createGA4HttpSink(options: GA4HttpOptions): AnalyticsSink;
134
+
135
+ /**
136
+ * GA4 `client-sdk` adapter — lazy gtag.js.
137
+ *
138
+ * The vendor library is NEVER a hard dependency and is NEVER bundled. On the
139
+ * first delivery the adapter:
140
+ * 1. installs the gtag stub + dataLayer,
141
+ * 2. pushes the Consent Mode default (if a bridge is configured),
142
+ * 3. lazily injects `https://www.googletagmanager.com/gtag/js?id=<id>`
143
+ * via a `<script>` tag (or an injected loader for tests/SSR),
144
+ * 4. runs `gtag('js', Date)` + `gtag('config', <id>, { send_page_view:false })`.
145
+ *
146
+ * Subsequent deliveries map each canonical envelope through the shared mapper
147
+ * and call `gtag('set', 'user_properties', …)` / `gtag('set', { user_id })` /
148
+ * `gtag('event', name, params)`.
149
+ *
150
+ * Consent Mode bridge: `init` pushes `consent: default`; `deliver` is only
151
+ * reached when the router's consent gate already allows this sink, at which
152
+ * point `consent: update` is pushed for the mapped GA4 consent types.
153
+ */
154
+
155
+ /**
156
+ * Create the GA4 gtag.js (`client-sdk`) sink. The vendor script is dynamically
157
+ * loaded on first delivery only — there is no static import of any GA4/gtag
158
+ * library, so `http`-mode consumers never pull this code path.
159
+ */
160
+ declare function createGA4ClientSdkSink(options: GA4ClientSdkOptions): AnalyticsSink;
161
+
162
+ /**
163
+ * Canonical envelope → GA4 mapping.
164
+ *
165
+ * One mapper, two consumers: the `client-sdk` adapter feeds the result into
166
+ * `gtag('event', name, params)` / `gtag('set', ...)`, and the `http` adapter
167
+ * serialises it into a Measurement Protocol `/mp/collect` payload. Keeping the
168
+ * mapping in one place guarantees the two modes stay behaviourally identical.
169
+ *
170
+ * Identity / param mapping (issue #216, epic #213):
171
+ * anonymousId → client_id (GA4 device/browser id)
172
+ * userId → user_id (GA4 User-ID, cross-device)
173
+ * properties → event params (track/page/screen/group payload)
174
+ * identify → user_properties (traits become GA4 user properties)
175
+ * sessionId → session_id param (so GA4 sessionisation can align)
176
+ *
177
+ * GA4 event-name normalisation: GA4 recommends snake_case event names and
178
+ * forbids spaces. `page` → `page_view`, `screen` → `screen_view` (GA4
179
+ * Enhanced-Measurement parity); everything else is lower_snake_cased.
180
+ */
181
+
182
+ /** A GA4 event ready for gtag.js or the Measurement Protocol. */
183
+ interface GA4Event {
184
+ /** GA4 event name (snake_case, no spaces). */
185
+ name: string;
186
+ /** Event params (GA4 caps these; we pass them through verbatim). */
187
+ params: Record<string, unknown>;
188
+ }
189
+ /** The fully-mapped result for a single canonical envelope. */
190
+ interface GA4Mapped {
191
+ /** GA4 client_id (from anonymousId). Always present. */
192
+ clientId: string;
193
+ /** GA4 user_id (from userId). Present only after identify. */
194
+ userId?: string;
195
+ /**
196
+ * GA4 user_properties (from identify/group traits). Each value is wrapped
197
+ * in `{ value }` as the Measurement Protocol requires; the gtag adapter
198
+ * unwraps when it calls `gtag('set', 'user_properties', ...)`.
199
+ */
200
+ userProperties?: Record<string, {
201
+ value: unknown;
202
+ }>;
203
+ /**
204
+ * The GA4 event to send. `identify` calls carry no event (they only set
205
+ * user_id / user_properties), so this is optional.
206
+ */
207
+ event?: GA4Event;
208
+ }
209
+ /** Lower_snake_case an arbitrary event/trait name for GA4. */
210
+ declare function toGa4Name(name: string): string;
211
+ /** Map a Segment call type + name to its GA4 event name. */
212
+ declare function ga4EventName(ev: AnalyticsEvent): string | undefined;
213
+ /**
214
+ * Map one canonical envelope to its GA4 representation. Pure — no transport,
215
+ * no vendor lib; both the gtag and Measurement-Protocol adapters consume this.
216
+ */
217
+ declare function mapEvent(ev: AnalyticsEvent): GA4Mapped;
218
+
219
+ export { type ConsentState, type GA4ClientSdkOptions, type GA4ConsentBridge, type GA4Event, type GA4HttpOptions, type GA4Mapped, type GA4SinkOptions, type GtagFn, createGA4ClientSdkSink, createGA4HttpSink, createGA4Sink, ga4EventName, mapEvent, toGa4Name };
@@ -0,0 +1,155 @@
1
+ import { AnalyticsSink, AnalyticsEvent } from '../analytics/index.cjs';
2
+
3
+ interface PostHogHttpSinkOptions {
4
+ /** PostHog project API key (the public, write-only key). */
5
+ apiKey: string;
6
+ /**
7
+ * Ingestion host. Default `https://us.i.posthog.com`. Use
8
+ * `https://eu.i.posthog.com`, a self-hosted host, or — recommended for
9
+ * production — your own reverse-proxy/relay path.
10
+ */
11
+ host?: string;
12
+ /** Sink name. Default `posthog`. */
13
+ name?: string;
14
+ /** Consent categories this sink requires. Default `['analytics']`. */
15
+ consentCategories?: string[];
16
+ /** Max retries for 429/5xx (exponential backoff). Default 3. */
17
+ maxRetries?: number;
18
+ /** Base backoff delay in ms. Default 500. */
19
+ backoffBaseMs?: number;
20
+ /** Injected fetch (defaults to global fetch). */
21
+ fetchImpl?: typeof fetch;
22
+ /** Injected sendBeacon (defaults to navigator.sendBeacon). */
23
+ beaconImpl?: (url: string, body: string) => boolean;
24
+ /** Soft per-batch byte cap. PostHog limit ≈ 20MB; default 1MB. */
25
+ maxBatchBytes?: number;
26
+ /** Soft per-event byte cap. Default 32KB. */
27
+ maxEventBytes?: number;
28
+ }
29
+ /**
30
+ * Create the PostHog `http`-mode sink (default). Pure protocol adapter —
31
+ * no `posthog-js`, safe in Node and the browser.
32
+ */
33
+ declare function createPostHogHttpSink(options: PostHogHttpSinkOptions): AnalyticsSink;
34
+
35
+ /**
36
+ * PostHog `client-sdk` sink — OPTIONAL, opt-in mode.
37
+ *
38
+ * For client-exclusive PostHog features (autocapture, feature flags,
39
+ * surveys, web experiments) that the protocol API alone cannot drive.
40
+ * `posthog-js` is loaded **lazily via dynamic `import()`** the first time the
41
+ * sink delivers, so a consumer who only uses the default `http` sink never
42
+ * pays the browser-library cost and `posthog-js` stays a fully optional peer.
43
+ *
44
+ * Session replay is deliberately NOT touched here — it lives in the separate
45
+ * `@refraction-ui/analytics-sink-posthog/replay` module so it is never on the
46
+ * event path and tree-shakes out unless explicitly enabled.
47
+ */
48
+ /** Minimal structural type for the bits of `posthog-js` we use. */
49
+ interface PostHogJs {
50
+ init(apiKey: string, options: Record<string, unknown>): void;
51
+ capture(event: string, properties?: Record<string, unknown>): void;
52
+ identify(distinctId: string, set?: Record<string, unknown>): void;
53
+ alias(alias: string, original?: string): void;
54
+ group(groupType: string, groupKey: string, properties?: Record<string, unknown>): void;
55
+ reset(): void;
56
+ }
57
+ interface PostHogClientSdkSinkOptions {
58
+ /** PostHog project API key. */
59
+ apiKey: string;
60
+ /** PostHog API host. Default `https://us.i.posthog.com`. */
61
+ host?: string;
62
+ /** Sink name. Default `posthog`. */
63
+ name?: string;
64
+ /** Consent categories this sink requires. Default `['analytics']`. */
65
+ consentCategories?: string[];
66
+ /**
67
+ * Extra `posthog-js` init options. `autocapture`, `capture_pageview`, and
68
+ * `session_recording` default to OFF — the canonical router owns the event
69
+ * path; replay is opt-in via the separate module.
70
+ */
71
+ posthogOptions?: Record<string, unknown>;
72
+ /**
73
+ * Injected loader (testing / custom bundling). Defaults to a lazy
74
+ * `import('posthog-js')`.
75
+ */
76
+ loadPostHog?: () => Promise<{
77
+ default: PostHogJs;
78
+ } | PostHogJs>;
79
+ }
80
+ /**
81
+ * Create the OPTIONAL PostHog `client-sdk`-mode sink. `posthog-js` is
82
+ * dynamically imported on first use, never at module load.
83
+ */
84
+ declare function createPostHogClientSdkSink(options: PostHogClientSdkSinkOptions): AnalyticsSink;
85
+
86
+ /** PostHog fan-out mode. `http` is the default (no browser library). */
87
+ type PostHogSinkMode = 'http' | 'client-sdk';
88
+ type PostHogSinkOptions = ({
89
+ mode?: 'http';
90
+ } & PostHogHttpSinkOptions) | ({
91
+ mode: 'client-sdk';
92
+ } & PostHogClientSdkSinkOptions);
93
+ /**
94
+ * Create a PostHog `AnalyticsSink`.
95
+ *
96
+ * - `mode: 'http'` (DEFAULT) — pure protocol adapter against PostHog's
97
+ * `/capture` + `/batch` API. No `posthog-js`. Server-relay friendly.
98
+ * - `mode: 'client-sdk'` — OPTIONAL. Lazily dynamic-imports `posthog-js`
99
+ * for client-exclusive features. Only loaded if this mode is selected.
100
+ *
101
+ * Session replay is NOT configurable here — import the separate
102
+ * `@refraction-ui/analytics-sink-posthog/replay` module to opt in. It is
103
+ * never on the event path and tree-shakes away when unused.
104
+ */
105
+ declare function createPostHogSink(options: PostHogSinkOptions): AnalyticsSink;
106
+
107
+ /**
108
+ * Canonical envelope → PostHog event mapping.
109
+ *
110
+ * This module is pure (no I/O, no transport) so the http sink and the
111
+ * client-sdk sink share exactly one mapping definition and it can be unit
112
+ * tested in isolation against a mock transport.
113
+ *
114
+ * PostHog identity model:
115
+ * - `distinct_id` is the single id PostHog buckets a person under.
116
+ * - Before `identify`, the anonymous visitor is keyed by `anonymousId`.
117
+ * - `identify` upgrades the person to the opaque app `userId`, sending
118
+ * `$set` traits and an `$anon_distinct_id` so PostHog stitches the
119
+ * pre-identify anonymous history onto the identified person.
120
+ * - `alias` emits PostHog's `$create_alias` linking `previousId` (alias)
121
+ * to the canonical `userId`/`anonymousId` (distinct_id).
122
+ * - `group` emits a `$groupidentify` event with `$group_set` traits.
123
+ *
124
+ * See https://posthog.com/docs/api/capture for the event shape.
125
+ */
126
+ /** A single PostHog capture event (the `/capture` and `/batch` item shape). */
127
+ interface PostHogEvent {
128
+ /** PostHog event name (Segment names map to `$pageview`/`$screen`/etc.). */
129
+ event: string;
130
+ /** The person bucket id. */
131
+ distinct_id: string;
132
+ /** Event + person/group properties. */
133
+ properties: Record<string, unknown>;
134
+ /** ISO-8601 client timestamp (PostHog corrects for skew server-side). */
135
+ timestamp: string;
136
+ /** Idempotency key — PostHog dedupes on this. Mirrors `messageId`. */
137
+ uuid: string;
138
+ }
139
+ /** Resolve the PostHog `distinct_id` for an envelope. */
140
+ declare function distinctId(ev: AnalyticsEvent): string;
141
+ /**
142
+ * Map one canonical envelope to one PostHog event.
143
+ *
144
+ * `track` → the event name verbatim, `properties` passed through.
145
+ * `page` → `$pageview` (PostHog's built-in pageview).
146
+ * `screen` → `$screen` with `$screen_name`.
147
+ * `identify` → `$identify` with `$set` traits + `$anon_distinct_id` stitch.
148
+ * `group` → `$groupidentify` with `$group_set` traits.
149
+ * `alias` → `$create_alias` linking `previousId` → distinct id.
150
+ */
151
+ declare function toPostHogEvent(ev: AnalyticsEvent): PostHogEvent;
152
+ /** Map a batch of canonical envelopes to PostHog events. */
153
+ declare function toPostHogBatch(batch: AnalyticsEvent[]): PostHogEvent[];
154
+
155
+ export { type PostHogClientSdkSinkOptions, type PostHogEvent, type PostHogHttpSinkOptions, type PostHogSinkMode, type PostHogSinkOptions, createPostHogClientSdkSink, createPostHogHttpSink, createPostHogSink, distinctId, toPostHogBatch, toPostHogEvent };
@@ -0,0 +1,155 @@
1
+ import { AnalyticsSink, AnalyticsEvent } from '../analytics/index.js';
2
+
3
+ interface PostHogHttpSinkOptions {
4
+ /** PostHog project API key (the public, write-only key). */
5
+ apiKey: string;
6
+ /**
7
+ * Ingestion host. Default `https://us.i.posthog.com`. Use
8
+ * `https://eu.i.posthog.com`, a self-hosted host, or — recommended for
9
+ * production — your own reverse-proxy/relay path.
10
+ */
11
+ host?: string;
12
+ /** Sink name. Default `posthog`. */
13
+ name?: string;
14
+ /** Consent categories this sink requires. Default `['analytics']`. */
15
+ consentCategories?: string[];
16
+ /** Max retries for 429/5xx (exponential backoff). Default 3. */
17
+ maxRetries?: number;
18
+ /** Base backoff delay in ms. Default 500. */
19
+ backoffBaseMs?: number;
20
+ /** Injected fetch (defaults to global fetch). */
21
+ fetchImpl?: typeof fetch;
22
+ /** Injected sendBeacon (defaults to navigator.sendBeacon). */
23
+ beaconImpl?: (url: string, body: string) => boolean;
24
+ /** Soft per-batch byte cap. PostHog limit ≈ 20MB; default 1MB. */
25
+ maxBatchBytes?: number;
26
+ /** Soft per-event byte cap. Default 32KB. */
27
+ maxEventBytes?: number;
28
+ }
29
+ /**
30
+ * Create the PostHog `http`-mode sink (default). Pure protocol adapter —
31
+ * no `posthog-js`, safe in Node and the browser.
32
+ */
33
+ declare function createPostHogHttpSink(options: PostHogHttpSinkOptions): AnalyticsSink;
34
+
35
+ /**
36
+ * PostHog `client-sdk` sink — OPTIONAL, opt-in mode.
37
+ *
38
+ * For client-exclusive PostHog features (autocapture, feature flags,
39
+ * surveys, web experiments) that the protocol API alone cannot drive.
40
+ * `posthog-js` is loaded **lazily via dynamic `import()`** the first time the
41
+ * sink delivers, so a consumer who only uses the default `http` sink never
42
+ * pays the browser-library cost and `posthog-js` stays a fully optional peer.
43
+ *
44
+ * Session replay is deliberately NOT touched here — it lives in the separate
45
+ * `@refraction-ui/analytics-sink-posthog/replay` module so it is never on the
46
+ * event path and tree-shakes out unless explicitly enabled.
47
+ */
48
+ /** Minimal structural type for the bits of `posthog-js` we use. */
49
+ interface PostHogJs {
50
+ init(apiKey: string, options: Record<string, unknown>): void;
51
+ capture(event: string, properties?: Record<string, unknown>): void;
52
+ identify(distinctId: string, set?: Record<string, unknown>): void;
53
+ alias(alias: string, original?: string): void;
54
+ group(groupType: string, groupKey: string, properties?: Record<string, unknown>): void;
55
+ reset(): void;
56
+ }
57
+ interface PostHogClientSdkSinkOptions {
58
+ /** PostHog project API key. */
59
+ apiKey: string;
60
+ /** PostHog API host. Default `https://us.i.posthog.com`. */
61
+ host?: string;
62
+ /** Sink name. Default `posthog`. */
63
+ name?: string;
64
+ /** Consent categories this sink requires. Default `['analytics']`. */
65
+ consentCategories?: string[];
66
+ /**
67
+ * Extra `posthog-js` init options. `autocapture`, `capture_pageview`, and
68
+ * `session_recording` default to OFF — the canonical router owns the event
69
+ * path; replay is opt-in via the separate module.
70
+ */
71
+ posthogOptions?: Record<string, unknown>;
72
+ /**
73
+ * Injected loader (testing / custom bundling). Defaults to a lazy
74
+ * `import('posthog-js')`.
75
+ */
76
+ loadPostHog?: () => Promise<{
77
+ default: PostHogJs;
78
+ } | PostHogJs>;
79
+ }
80
+ /**
81
+ * Create the OPTIONAL PostHog `client-sdk`-mode sink. `posthog-js` is
82
+ * dynamically imported on first use, never at module load.
83
+ */
84
+ declare function createPostHogClientSdkSink(options: PostHogClientSdkSinkOptions): AnalyticsSink;
85
+
86
+ /** PostHog fan-out mode. `http` is the default (no browser library). */
87
+ type PostHogSinkMode = 'http' | 'client-sdk';
88
+ type PostHogSinkOptions = ({
89
+ mode?: 'http';
90
+ } & PostHogHttpSinkOptions) | ({
91
+ mode: 'client-sdk';
92
+ } & PostHogClientSdkSinkOptions);
93
+ /**
94
+ * Create a PostHog `AnalyticsSink`.
95
+ *
96
+ * - `mode: 'http'` (DEFAULT) — pure protocol adapter against PostHog's
97
+ * `/capture` + `/batch` API. No `posthog-js`. Server-relay friendly.
98
+ * - `mode: 'client-sdk'` — OPTIONAL. Lazily dynamic-imports `posthog-js`
99
+ * for client-exclusive features. Only loaded if this mode is selected.
100
+ *
101
+ * Session replay is NOT configurable here — import the separate
102
+ * `@refraction-ui/analytics-sink-posthog/replay` module to opt in. It is
103
+ * never on the event path and tree-shakes away when unused.
104
+ */
105
+ declare function createPostHogSink(options: PostHogSinkOptions): AnalyticsSink;
106
+
107
+ /**
108
+ * Canonical envelope → PostHog event mapping.
109
+ *
110
+ * This module is pure (no I/O, no transport) so the http sink and the
111
+ * client-sdk sink share exactly one mapping definition and it can be unit
112
+ * tested in isolation against a mock transport.
113
+ *
114
+ * PostHog identity model:
115
+ * - `distinct_id` is the single id PostHog buckets a person under.
116
+ * - Before `identify`, the anonymous visitor is keyed by `anonymousId`.
117
+ * - `identify` upgrades the person to the opaque app `userId`, sending
118
+ * `$set` traits and an `$anon_distinct_id` so PostHog stitches the
119
+ * pre-identify anonymous history onto the identified person.
120
+ * - `alias` emits PostHog's `$create_alias` linking `previousId` (alias)
121
+ * to the canonical `userId`/`anonymousId` (distinct_id).
122
+ * - `group` emits a `$groupidentify` event with `$group_set` traits.
123
+ *
124
+ * See https://posthog.com/docs/api/capture for the event shape.
125
+ */
126
+ /** A single PostHog capture event (the `/capture` and `/batch` item shape). */
127
+ interface PostHogEvent {
128
+ /** PostHog event name (Segment names map to `$pageview`/`$screen`/etc.). */
129
+ event: string;
130
+ /** The person bucket id. */
131
+ distinct_id: string;
132
+ /** Event + person/group properties. */
133
+ properties: Record<string, unknown>;
134
+ /** ISO-8601 client timestamp (PostHog corrects for skew server-side). */
135
+ timestamp: string;
136
+ /** Idempotency key — PostHog dedupes on this. Mirrors `messageId`. */
137
+ uuid: string;
138
+ }
139
+ /** Resolve the PostHog `distinct_id` for an envelope. */
140
+ declare function distinctId(ev: AnalyticsEvent): string;
141
+ /**
142
+ * Map one canonical envelope to one PostHog event.
143
+ *
144
+ * `track` → the event name verbatim, `properties` passed through.
145
+ * `page` → `$pageview` (PostHog's built-in pageview).
146
+ * `screen` → `$screen` with `$screen_name`.
147
+ * `identify` → `$identify` with `$set` traits + `$anon_distinct_id` stitch.
148
+ * `group` → `$groupidentify` with `$group_set` traits.
149
+ * `alias` → `$create_alias` linking `previousId` → distinct id.
150
+ */
151
+ declare function toPostHogEvent(ev: AnalyticsEvent): PostHogEvent;
152
+ /** Map a batch of canonical envelopes to PostHog events. */
153
+ declare function toPostHogBatch(batch: AnalyticsEvent[]): PostHogEvent[];
154
+
155
+ export { type PostHogClientSdkSinkOptions, type PostHogEvent, type PostHogHttpSinkOptions, type PostHogSinkMode, type PostHogSinkOptions, createPostHogClientSdkSink, createPostHogHttpSink, createPostHogSink, distinctId, toPostHogBatch, toPostHogEvent };
@@ -0,0 +1,78 @@
1
+ /**
2
+ * @refraction-ui/analytics-sink-posthog/replay
3
+ *
4
+ * OPTIONAL, lazy session-replay (rrweb, via `posthog-js`).
5
+ *
6
+ * Hard guarantees this module exists to enforce:
7
+ *
8
+ * 1. **Off by default.** Nothing in the main `@refraction-ui/analytics-sink-posthog`
9
+ * entry imports this file, so it (and `posthog-js`, and rrweb) are fully
10
+ * tree-shaken out of any bundle that does not explicitly import it.
11
+ * 2. **Never on the event path.** Replay is not an `AnalyticsSink`. It does
12
+ * not see, transform, or block canonical envelopes. `track`/`identify`/…
13
+ * keep flowing through the sink even if replay never starts or fails.
14
+ * 3. **Privacy/consent gated.** `startSessionReplay` refuses to start unless
15
+ * a consent predicate returns true, and re-checks it; `stop()` tears the
16
+ * recorder down. Masking defaults are maximally private.
17
+ * 4. **Lazy.** `posthog-js` is `import()`-ed only when `startSessionReplay`
18
+ * is actually called.
19
+ *
20
+ * This is a thin controller around `posthog-js`'s built-in rrweb session
21
+ * recording — we do not bundle rrweb ourselves.
22
+ */
23
+ /** Minimal structural view of the `posthog-js` replay surface. */
24
+ interface PostHogReplay {
25
+ init(apiKey: string, options: Record<string, unknown>): void;
26
+ startSessionRecording(): void;
27
+ stopSessionRecording(): void;
28
+ sessionRecordingStarted?(): boolean;
29
+ }
30
+ interface SessionReplayOptions {
31
+ /** PostHog project API key. */
32
+ apiKey: string;
33
+ /** PostHog API host. Default `https://us.i.posthog.com`. */
34
+ host?: string;
35
+ /**
36
+ * Consent predicate. Replay starts ONLY when this returns `true`, and is
37
+ * re-checked by `enforceConsent()`. There is no default-allow: if omitted,
38
+ * replay is treated as NOT consented and will not start.
39
+ */
40
+ hasConsent?: () => boolean;
41
+ /**
42
+ * rrweb masking. Defaults are maximally private: all text + all inputs
43
+ * masked. Override deliberately and document the privacy review.
44
+ */
45
+ maskAllText?: boolean;
46
+ maskAllInputs?: boolean;
47
+ /** Extra `posthog-js` session_recording options (advanced). */
48
+ recordingOptions?: Record<string, unknown>;
49
+ /**
50
+ * Injected loader (testing / custom bundling). Defaults to a lazy
51
+ * `import('posthog-js')`.
52
+ */
53
+ loadPostHog?: () => Promise<{
54
+ default: PostHogReplay;
55
+ } | PostHogReplay>;
56
+ }
57
+ /** Handle returned by {@link startSessionReplay}. */
58
+ interface SessionReplayHandle {
59
+ /** True if the rrweb recorder is currently running. */
60
+ readonly recording: boolean;
61
+ /**
62
+ * Re-evaluate the consent predicate. If consent was revoked, recording is
63
+ * stopped. Call this from your consent-change handler.
64
+ */
65
+ enforceConsent(): void;
66
+ /** Stop recording and release the recorder. Idempotent. */
67
+ stop(): void;
68
+ }
69
+ /**
70
+ * Start PostHog/rrweb session replay. Resolves to a handle, or to a
71
+ * non-recording handle if consent is not granted (it never throws on the
72
+ * consent path — replay simply does not start).
73
+ *
74
+ * `posthog-js` is dynamically imported here and nowhere else.
75
+ */
76
+ declare function startSessionReplay(options: SessionReplayOptions): Promise<SessionReplayHandle>;
77
+
78
+ export { type SessionReplayHandle, type SessionReplayOptions, startSessionReplay };