@convex-internal/web-analytics 2.0.1 → 2.2.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
@@ -29,15 +29,35 @@ capture("www-login", undefined, {
29
29
  });
30
30
  ```
31
31
 
32
- ### Consent banner
32
+ ### `getDistinctId`
33
+
34
+ PostHog's identifier for the current visitor. After consent is granted it is
35
+ stored in a cookie on `.convex.dev`, so every Convex site in the browser uses
36
+ the same ID.
37
+
38
+ When this helper returns a value, consent has been granted. Pass that ID to a
39
+ third-party tool so you can match that tool's events or sessions, such as a
40
+ support chat, back to the same visitor in PostHog.
33
41
 
34
- The banner shows when `consent` is `"undecided"`. It records
35
- `cookie_consent_shown` when it appears and `cookie_consent_decided` with
36
- `{ decision: "accepted" | "declined" }` just before the choice is saved.
42
+ It's expected that a call on first load is always `undefined`, so call again
43
+ after consent is granted.
37
44
 
38
- The banner stays hidden while Global Privacy Control is on, because that
39
- resolves `consent` to `"declined"` rather than `"undecided"`. GPC does not
40
- write the cookie; see [Cookie behavior](#cookie-behavior).
45
+ It returns one of:
46
+
47
+ - **`undefined`**. PostHog is not initialized (localhost, non-production, or
48
+ still loading), or consent has not been granted. Before consent, PostHog
49
+ still has a cookieless ID, but it is not stable across sites or even page
50
+ reloads, so returning it would create noise rather than being useful.
51
+ - **Anonymous UUID**, such as `"019a7d8f-9190-7844-a5c0-73d461a3a5d1"`.
52
+ Consent has been granted but this browser has not logged into the
53
+ dashboard where the user is identified.
54
+ - **Convex profile ID**, such as `"4281"`. Consent has been granted and this
55
+ browser has logged into the dashboard.
56
+
57
+ ### Consent banner
58
+
59
+ The banner is visible when `consent` is `"undecided"`. It is hidden while
60
+ `consent` is `"unknown"`, `"consented"`, or `"declined"`.
41
61
 
42
62
  Theme with `--wa-banner-*` variables on `.wa-banner`:
43
63
 
@@ -66,6 +86,15 @@ Theme with `--wa-banner-*` variables on `.wa-banner`:
66
86
 
67
87
  Buttons default to 80% opacity and go to 100% on hover or keyboard focus.
68
88
 
89
+ ### Consent events
90
+
91
+ `cookie_consent_shown` is recorded when the banner appears.
92
+ `cookie_consent_decided` is recorded with
93
+ `{ decision: "accepted" | "declined", source: "banner" }` just before the
94
+ choice is saved, and with `{ decision: "declined", source: "gpc" }` when
95
+ the GPC preference is applied. Historic events without `source` are banner
96
+ decisions.
97
+
69
98
  ### Cookie behavior
70
99
 
71
100
  Once the cookie has been read, `consent` is resolved in this order:
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export { capture, type CaptureOptions } from "./capture.js";
2
+ export { getDistinctId } from "./posthog.js";
2
3
  export type { ConsentStatus } from "./consent.js";
package/dist/index.js CHANGED
@@ -1 +1,2 @@
1
1
  export { capture } from "./capture.js";
2
+ export { getDistinctId } from "./posthog.js";
package/dist/posthog.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { ConsentStatus } from "./consent.js";
2
2
  export declare const POSTHOG_PUBLIC_PROJECT_TOKEN = "phc_JDNTRxeh2li2sQTRO0IcOYMJcp8fPs5nTK9TU751nQK";
3
3
  export declare function initPostHog(): void;
4
+ export declare function getDistinctId(): string | undefined;
4
5
  export declare function syncPostHogConsent(consent: ConsentStatus): void;
package/dist/posthog.js CHANGED
@@ -32,6 +32,16 @@ export function initPostHog() {
32
32
  person_profiles: "identified_only",
33
33
  });
34
34
  }
35
+ export function getDistinctId() {
36
+ if (!posthog.__loaded) {
37
+ return undefined;
38
+ }
39
+ // Cookieless IDs change across sites and reloads, so aren't useful to return.
40
+ if (posthog.get_explicit_consent_status() !== "granted") {
41
+ return undefined;
42
+ }
43
+ return posthog.get_distinct_id();
44
+ }
35
45
  export function syncPostHogConsent(consent) {
36
46
  if (!posthog.__loaded) {
37
47
  return;
@@ -1,14 +1,16 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useRef } from "react";
4
- import { bannerCopy } from "../strings.js";
5
4
  import { capture } from "../capture.js";
5
+ import { hasGPC } from "../consent.js";
6
+ import { bannerCopy } from "../strings.js";
6
7
  import { useConsent } from "./useConsent.js";
7
8
  import "../banner.css";
8
9
  export function ConsentBanner() {
9
10
  const { consent, setConsent } = useConsent();
10
11
  const shouldShow = consent === "undecided";
11
12
  const wasShowing = useRef(false);
13
+ const recordedGPCDecision = useRef(false);
12
14
  useEffect(() => {
13
15
  if (shouldShow && !wasShowing.current) {
14
16
  // Captures once when the banner becomes visible, not on later renders.
@@ -16,13 +18,22 @@ export function ConsentBanner() {
16
18
  }
17
19
  wasShowing.current = shouldShow;
18
20
  }, [shouldShow]);
21
+ useEffect(() => {
22
+ if (consent === "declined" && hasGPC() && !recordedGPCDecision.current) {
23
+ recordedGPCDecision.current = true;
24
+ capture("cookie_consent_decided", {
25
+ decision: "declined",
26
+ source: "gpc",
27
+ });
28
+ }
29
+ }, [consent]);
19
30
  if (!shouldShow) {
20
31
  return null;
21
32
  }
22
33
  const decide = (decision) => {
23
34
  try {
24
35
  // Captures before the cookie write so decline is captured cookieless.
25
- capture("cookie_consent_decided", { decision });
36
+ capture("cookie_consent_decided", { decision, source: "banner" });
26
37
  }
27
38
  finally {
28
39
  setConsent(decision === "accepted" ? "consented" : "declined");
@@ -9,6 +9,7 @@
9
9
 
10
10
  let isReady = $state(false);
11
11
  let cookieValue = $state<boolean | undefined>(undefined);
12
+ let recordedGPCDecision = false;
12
13
 
13
14
  const consent = $derived(resolveConsent(isReady, cookieValue, hasGPC()));
14
15
  const shouldShowBanner = $derived(consent === "undecided");
@@ -60,6 +61,17 @@
60
61
  wasShowingBanner = shouldShowBanner;
61
62
  });
62
63
 
64
+ $effect(() => {
65
+ if (consent === "declined" && hasGPC() && !recordedGPCDecision) {
66
+ // GPC hides the banner, so record the decline here instead of a click.
67
+ recordedGPCDecision = true;
68
+ capture("cookie_consent_decided", {
69
+ decision: "declined",
70
+ source: "gpc",
71
+ });
72
+ }
73
+ });
74
+
63
75
  function setConsent(status: "consented" | "declined") {
64
76
  setCookie(COOKIE_NAME, status === "consented" ? "true" : "false");
65
77
  cookieValue = status === "consented";
@@ -68,7 +80,7 @@
68
80
  function decide(decision: "accepted" | "declined") {
69
81
  try {
70
82
  // Captures before the cookie write so decline is captured cookieless.
71
- capture("cookie_consent_decided", { decision });
83
+ capture("cookie_consent_decided", { decision, source: "banner" });
72
84
  } finally {
73
85
  setConsent(decision === "accepted" ? "consented" : "declined");
74
86
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@convex-internal/web-analytics",
3
- "version": "2.0.1",
3
+ "version": "2.2.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {