@convex-internal/web-analytics 1.0.3 → 1.1.1

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
@@ -14,14 +14,13 @@ provider.
14
14
 
15
15
  The provider starts analytics in production, except on localhost, syncs
16
16
  consent to PostHog, and shows the consent banner when the visitor has not
17
- chosen yet. Pass `posthogConfig` for site-specific options, such as the
18
- website's `dub_id` campaign param:
17
+ chosen yet.
19
18
 
20
- ```tsx
21
- <WebAnalyticsProvider posthogConfig={{ custom_campaign_params: ["dub_id"] }}>
22
- {children}
23
- </WebAnalyticsProvider>
24
- ```
19
+ PostHog already copies standard campaign params (UTMs, `gclid`, `rdt_cid`,
20
+ `fbclid`, and other common click IDs) from the landing URL onto events. This
21
+ package also captures `dub_id`. After consent, PostHog creates an anonymous
22
+ person profile. Campaign params still available at that point can be stored as
23
+ `$initial_*`, supporting person-level attribution before login.
25
24
 
26
25
  ### `capture`
27
26
 
@@ -1,22 +1,17 @@
1
- import { type PostHogConfig } from "posthog-js";
2
1
  import { type ReactNode } from "react";
3
- export type { PostHogConfig };
2
+ export declare const POSTHOG_PUBLIC_PROJECT_TOKEN = "phc_JDNTRxeh2li2sQTRO0IcOYMJcp8fPs5nTK9TU751nQK";
4
3
  /**
5
- * `"unknown"` until the cookie has been read on the client (SSR and first
6
- * render). `"undecided"` when there is no recognized choice yet. `"consented"`
7
- * when the cookie is `true` and Global Privacy Control (GPC) is not set.
8
- * `"declined"` when the cookie is `false`, or while GPC is set.
9
- *
10
- * GPC does not write or change the cookie. If it is later turned off, the
11
- * stored cookie (or the banner, if none) applies again.
4
+ * `"unknown"` until the cookie has been read (SSR and first client render).
5
+ * `"undecided"` when it has been read and there is no recognized choice.
6
+ * `"consented"` when the cookie is `true` and Global Privacy Control is off.
7
+ * `"declined"` when the cookie is `false`, or while GPC is on.
12
8
  */
13
9
  export type ConsentStatus = "unknown" | "undecided" | "consented" | "declined";
14
10
  export interface WebAnalyticsProviderProps {
15
11
  children: ReactNode;
16
- posthogConfig?: Partial<PostHogConfig>;
17
12
  }
18
13
  export declare const ConsentContext: import("react").Context<{
19
14
  consent: ConsentStatus;
20
15
  setConsent: (status: "consented" | "declined") => void;
21
16
  } | null>;
22
- export declare function WebAnalyticsProvider({ children, posthogConfig, }: WebAnalyticsProviderProps): import("react").JSX.Element;
17
+ export declare function WebAnalyticsProvider({ children }: WebAnalyticsProviderProps): import("react").JSX.Element;
@@ -4,9 +4,15 @@ import { posthog } from "posthog-js";
4
4
  import { createContext, useEffect } from "react";
5
5
  import { ConsentBanner } from "./ConsentBanner.js";
6
6
  import { useBooleanCookie } from "./useCookie.js";
7
- const COOKIE_NAME = "allowsCookies";
8
- const POSTHOG_KEY = "phc_JDNTRxeh2li2sQTRO0IcOYMJcp8fPs5nTK9TU751nQK";
7
+ // PostHog project token for event ingest and feature flags. `phc_` tokens are
8
+ // public by design (browser JS, no env var) and cannot read private project data.
9
+ // https://posthog.com/docs/privacy#is-it-ok-for-my-api-key-to-be-exposed-and-public
10
+ export const POSTHOG_PUBLIC_PROJECT_TOKEN = "phc_JDNTRxeh2li2sQTRO0IcOYMJcp8fPs5nTK9TU751nQK";
9
11
  const POSTHOG_HOST = "https://ap.convex.dev";
12
+ const COOKIE_NAME = "allowsCookies";
13
+ // GPC wins over the stored cookie and does not write it, so turning GPC off
14
+ // restores the saved choice (or the banner if there is none). A missing or
15
+ // unrecognized cookie after the read defaults to `"undecided"`.
10
16
  function resolveConsent(isReady, cookieValue, gpcOptOut) {
11
17
  if (!isReady) {
12
18
  return "unknown";
@@ -23,27 +29,23 @@ function resolveConsent(isReady, cookieValue, gpcOptOut) {
23
29
  return "undecided";
24
30
  }
25
31
  export const ConsentContext = createContext(null);
26
- export function WebAnalyticsProvider({ children, posthogConfig, }) {
32
+ export function WebAnalyticsProvider({ children }) {
27
33
  const { value, isReady, setValue } = useBooleanCookie(COOKIE_NAME);
28
34
  const gpcOptOut = typeof navigator !== "undefined" && navigator.globalPrivacyControl === true;
29
35
  const consent = resolveConsent(isReady, value, gpcOptOut);
36
+ // Initialize PostHog once, in cookieless mode. Consent is applied below.
30
37
  useEffect(() => {
31
- // Prevent PostHog from initializing in development or on localhost.
38
+ if (posthog.__loaded) {
39
+ return;
40
+ }
41
+ // Keep local/dev sessions out of production PostHog data.
32
42
  if (process.env.NODE_ENV !== "production" ||
33
43
  window.location.hostname === "localhost" ||
34
44
  window.location.hostname === "127.0.0.1" ||
35
45
  window.location.hostname === "[::1]") {
36
46
  return;
37
47
  }
38
- // If PostHog is already initialized, update its config.
39
- if (posthog.__loaded) {
40
- if (posthogConfig) {
41
- posthog.set_config(posthogConfig);
42
- }
43
- return;
44
- }
45
- // Initialize PostHog.
46
- posthog.init(POSTHOG_KEY, {
48
+ posthog.init(POSTHOG_PUBLIC_PROJECT_TOKEN, {
47
49
  api_host: POSTHOG_HOST,
48
50
  ui_host: "https://us.posthog.com/",
49
51
  // Automatically capture navigations using the History API.
@@ -53,28 +55,33 @@ export function WebAnalyticsProvider({ children, posthogConfig, }) {
53
55
  // tracking. See more: https://posthog.com/tutorials/cookieless-tracking
54
56
  opt_out_capturing_by_default: true,
55
57
  cookieless_mode: "on_reject",
56
- // Site-specific options to add or override the defaults.
57
- ...posthogConfig,
58
+ // Extends PostHog's built-in campaign params (gclid, rdt_cid, UTMs, etc.)
59
+ // onto events.
60
+ custom_campaign_params: ["dub_id"],
61
+ // Person profiles are enabled later, after consent is received.
62
+ person_profiles: "identified_only",
58
63
  });
59
- }, [posthogConfig]);
60
- // One-way sync: make PostHog match the resolved consent status.
64
+ }, []);
65
+ // Keep PostHog capturing in sync with consent.
61
66
  useEffect(() => {
62
- // No decision yet or PostHog is not initialized.
63
- if (consent === "unknown" || consent === "undecided" || !posthog.__loaded) {
67
+ if (!posthog.__loaded) {
64
68
  return;
65
69
  }
66
- const posthogConsent = posthog.get_explicit_consent_status();
67
- const targetConsent = consent === "consented" ? "granted" : "denied";
68
- if (posthogConsent === targetConsent) {
70
+ // Skip until there's a real choice made.
71
+ if (consent === "unknown" || consent === "undecided") {
69
72
  return;
70
73
  }
74
+ // Skip opt-in/out when already in sync so returning visitors don't get a
75
+ // duplicate `$opt_in`.
71
76
  if (consent === "consented") {
72
- // Records an `$opt_in` event, which is distinct from ConsentBanner's
73
- // `cookie_consent_decided` as it also fires when we sync an existing
74
- // allowsCookies cookie for returning visitors.
75
- posthog.opt_in_capturing();
77
+ if (posthog.get_explicit_consent_status() !== "granted") {
78
+ posthog.opt_in_capturing();
79
+ }
80
+ // Opting in replaces the cookieless identity with a persistent one, so we
81
+ // create the profile afterward. This is a no-op if it already exists.
82
+ posthog.createPersonProfile();
76
83
  }
77
- else {
84
+ else if (posthog.get_explicit_consent_status() !== "denied") {
78
85
  posthog.opt_out_capturing();
79
86
  }
80
87
  }, [consent]);
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { WebAnalyticsProvider, type WebAnalyticsProviderProps, type PostHogConfig, type ConsentStatus, } from "./WebAnalyticsProvider.js";
1
+ export { WebAnalyticsProvider, type WebAnalyticsProviderProps, type ConsentStatus, } from "./WebAnalyticsProvider.js";
2
2
  export { Consented, type ConsentedProps } from "./Consented.js";
3
3
  export { useConsent } from "./useConsent.js";
4
4
  export { capture, type CaptureOptions } from "./capture.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@convex-internal/web-analytics",
3
- "version": "1.0.3",
3
+ "version": "1.1.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {