@burdenoff/website-sdk 2026.1031.1 → 2026.1031.3

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
@@ -339,7 +339,13 @@ publishes audio, video or data, and it sends the avatar nothing to say.
339
339
  page unloads. The server ends sessions on its own deadlines as well.
340
340
  - **Consent copy is versioned.** The panel shows a consent card only when it
341
341
  holds reviewed copy for exactly the version the server names (the current
342
- English copy is `web-consent-2026-11`); otherwise the view stays as it was.
342
+ English copy is `web-consent-2026-10-31`, and `web-consent-2026-11` stays for
343
+ servers that still name it); otherwise the view stays as it was.
344
+ - **How a session ended.** When the worker leaves, the server closes the room,
345
+ or the session's time runs out with the room still open, the view says only
346
+ "The live avatar has ended." (key `finished`). The browser
347
+ cannot tell the avatar reading to the end from a server-side stop or an
348
+ apps-first hand-over, so it does not say which.
343
349
  - **Requests.** All three go to the gateway without credentials. The create
344
350
  request carries `content-type: application/json`, `idempotency-key` (a UUID
345
351
  v4) and `x-web-avatar-bot-token` (the Turnstile token, which the gateway
@@ -24,4 +24,44 @@ interface RybbitAnalyticsProps {
24
24
  */
25
25
  declare function RybbitAnalytics({ siteId, scriptUrl, enabled, }: RybbitAnalyticsProps): null;
26
26
 
27
- export { RybbitAnalytics, type RybbitAnalyticsProps };
27
+ /**
28
+ * Website analytics events (Rybbit custom events).
29
+ *
30
+ * Product websites load the Rybbit script, which records pageviews on its own.
31
+ * Funnel facts that are not a pageview (for example a successful waitlist
32
+ * signup) are sent through `trackWebsiteEvent`, which calls `window.rybbit.event`
33
+ * when the script is present and does nothing otherwise.
34
+ *
35
+ * Privacy rules (BOFF-3167):
36
+ * - Event names come from the closed `WebsiteEventName` union.
37
+ * - Properties are short, low-cardinality strings, numbers or booleans. Strings
38
+ * longer than `MAX_PROPERTY_LENGTH`, values that look like an email address,
39
+ * and any other value type are dropped, so a caller cannot leak an email,
40
+ * name or free text into the analytics provider by mistake.
41
+ * - Tracking never throws: analytics must not break a signup.
42
+ */
43
+ /** Custom events product websites emit. */
44
+ type WebsiteEventName = "waitlist.signed_up";
45
+ type WebsiteEventPropertyValue = string | number | boolean;
46
+ type WebsiteEventProperties = Record<string, WebsiteEventPropertyValue>;
47
+ declare global {
48
+ interface Window {
49
+ /** Set by the Rybbit tracking script (https://app.rybbit.io/api/script.js). */
50
+ rybbit?: {
51
+ event?: (name: string, properties?: Record<string, WebsiteEventPropertyValue>) => void;
52
+ };
53
+ }
54
+ }
55
+ /** Longest string property value that is forwarded. */
56
+ declare const MAX_PROPERTY_LENGTH = 64;
57
+ /**
58
+ * Keeps only short, non-identifying primitive values. Exported for tests.
59
+ */
60
+ declare function sanitizeWebsiteEventProperties(properties: WebsiteEventProperties | undefined): WebsiteEventProperties;
61
+ /**
62
+ * Sends a custom event to Rybbit when the tracking script is loaded.
63
+ * Returns true when the event was handed to Rybbit.
64
+ */
65
+ declare function trackWebsiteEvent(name: WebsiteEventName, properties?: WebsiteEventProperties): boolean;
66
+
67
+ export { MAX_PROPERTY_LENGTH, RybbitAnalytics, type RybbitAnalyticsProps, type WebsiteEventName, type WebsiteEventProperties, type WebsiteEventPropertyValue, sanitizeWebsiteEventProperties, trackWebsiteEvent };
@@ -24,4 +24,44 @@ interface RybbitAnalyticsProps {
24
24
  */
25
25
  declare function RybbitAnalytics({ siteId, scriptUrl, enabled, }: RybbitAnalyticsProps): null;
26
26
 
27
- export { RybbitAnalytics, type RybbitAnalyticsProps };
27
+ /**
28
+ * Website analytics events (Rybbit custom events).
29
+ *
30
+ * Product websites load the Rybbit script, which records pageviews on its own.
31
+ * Funnel facts that are not a pageview (for example a successful waitlist
32
+ * signup) are sent through `trackWebsiteEvent`, which calls `window.rybbit.event`
33
+ * when the script is present and does nothing otherwise.
34
+ *
35
+ * Privacy rules (BOFF-3167):
36
+ * - Event names come from the closed `WebsiteEventName` union.
37
+ * - Properties are short, low-cardinality strings, numbers or booleans. Strings
38
+ * longer than `MAX_PROPERTY_LENGTH`, values that look like an email address,
39
+ * and any other value type are dropped, so a caller cannot leak an email,
40
+ * name or free text into the analytics provider by mistake.
41
+ * - Tracking never throws: analytics must not break a signup.
42
+ */
43
+ /** Custom events product websites emit. */
44
+ type WebsiteEventName = "waitlist.signed_up";
45
+ type WebsiteEventPropertyValue = string | number | boolean;
46
+ type WebsiteEventProperties = Record<string, WebsiteEventPropertyValue>;
47
+ declare global {
48
+ interface Window {
49
+ /** Set by the Rybbit tracking script (https://app.rybbit.io/api/script.js). */
50
+ rybbit?: {
51
+ event?: (name: string, properties?: Record<string, WebsiteEventPropertyValue>) => void;
52
+ };
53
+ }
54
+ }
55
+ /** Longest string property value that is forwarded. */
56
+ declare const MAX_PROPERTY_LENGTH = 64;
57
+ /**
58
+ * Keeps only short, non-identifying primitive values. Exported for tests.
59
+ */
60
+ declare function sanitizeWebsiteEventProperties(properties: WebsiteEventProperties | undefined): WebsiteEventProperties;
61
+ /**
62
+ * Sends a custom event to Rybbit when the tracking script is loaded.
63
+ * Returns true when the event was handed to Rybbit.
64
+ */
65
+ declare function trackWebsiteEvent(name: WebsiteEventName, properties?: WebsiteEventProperties): boolean;
66
+
67
+ export { MAX_PROPERTY_LENGTH, RybbitAnalytics, type RybbitAnalyticsProps, type WebsiteEventName, type WebsiteEventProperties, type WebsiteEventPropertyValue, sanitizeWebsiteEventProperties, trackWebsiteEvent };
@@ -31,6 +31,46 @@ function RybbitAnalytics({
31
31
  return null;
32
32
  }
33
33
 
34
+ // src/analytics/events.ts
35
+ var MAX_PROPERTY_LENGTH = 64;
36
+ var EMAIL_LIKE = /[^\s@]+@[^\s@]+/;
37
+ function sanitizeWebsiteEventProperties(properties) {
38
+ const safe = {};
39
+ if (!properties) return safe;
40
+ for (const [key, value] of Object.entries(properties)) {
41
+ if (typeof value === "boolean") {
42
+ safe[key] = value;
43
+ } else if (typeof value === "number") {
44
+ if (Number.isFinite(value)) safe[key] = value;
45
+ } else if (typeof value === "string") {
46
+ const trimmed = value.trim();
47
+ if (trimmed.length > 0 && trimmed.length <= MAX_PROPERTY_LENGTH && !EMAIL_LIKE.test(trimmed)) {
48
+ safe[key] = trimmed;
49
+ }
50
+ }
51
+ }
52
+ return safe;
53
+ }
54
+ function trackWebsiteEvent(name, properties) {
55
+ if (typeof window === "undefined") return false;
56
+ try {
57
+ const rybbit = window.rybbit;
58
+ if (typeof rybbit?.event !== "function") return false;
59
+ const safe = sanitizeWebsiteEventProperties(properties);
60
+ if (Object.keys(safe).length > 0) {
61
+ rybbit.event(name, safe);
62
+ } else {
63
+ rybbit.event(name);
64
+ }
65
+ return true;
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+
71
+ exports.MAX_PROPERTY_LENGTH = MAX_PROPERTY_LENGTH;
34
72
  exports.RybbitAnalytics = RybbitAnalytics;
73
+ exports.sanitizeWebsiteEventProperties = sanitizeWebsiteEventProperties;
74
+ exports.trackWebsiteEvent = trackWebsiteEvent;
35
75
  //# sourceMappingURL=index.js.map
36
76
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/analytics/rybbit.tsx"],"names":["useEffect"],"mappings":";;;;AAoDO,SAAS,eAAA,CAAgB;AAAA,EAC9B,MAAA;AAAA,EACA,SAAA,GAAY,+CAAA;AAAA,EACZ,OAAA,GAAU;AACZ,CAAA,EAAyB;AACvB,EAAAA,eAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,OAAA,IAAW,CAAC,MAAA,EAAQ;AAGzB,IAAA,MAAM,iBAAiB,QAAA,CAAS,aAAA;AAAA,MAC9B,wBAAwB,MAAM,CAAA,EAAA;AAAA,KAChC;AACA,IAAA,IAAI,cAAA,EAAgB;AAQpB,IAAA,MAAM,MAAA,GAAS,QAAA,CAAS,aAAA,CAAc,QAAQ,CAAA;AAC9C,IAAA,MAAA,CAAO,GAAA,GAAM,SAAA;AACb,IAAA,MAAA,CAAO,KAAA,GAAQ,IAAA;AACf,IAAA,MAAA,CAAO,WAAA,GAAc,WAAA;AACrB,IAAA,MAAA,CAAO,YAAA,CAAa,gBAAgB,MAAM,CAAA;AAC1C,IAAA,QAAA,CAAS,IAAA,CAAK,YAAY,MAAM,CAAA;AAGhC,IAAA,OAAO,MAAM;AACX,MAAA,MAAM,iBAAiB,QAAA,CAAS,aAAA;AAAA,QAC9B,wBAAwB,MAAM,CAAA,EAAA;AAAA,OAChC;AACA,MAAA,IAAI,cAAA,EAAgB;AAClB,QAAA,QAAA,CAAS,IAAA,CAAK,YAAY,cAAc,CAAA;AAAA,MAC1C;AAAA,IACF,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,MAAA,EAAQ,SAAA,EAAW,OAAO,CAAC,CAAA;AAG/B,EAAA,OAAO,IAAA;AACT","file":"index.js","sourcesContent":["/**\n * Rybbit Analytics Component\n *\n * Injects the Rybbit tracking script into the page head.\n * Works with both Vite (CSR) and Next.js (SSR/CSR) applications.\n *\n * @example\n * ```tsx\n * // In your app layout or root component\n * import { RybbitAnalytics } from '@burdenoff/website-sdk/analytics'\n *\n * function App() {\n * return (\n * <>\n * <RybbitAnalytics siteId=\"1\" />\n * <Router>...</Router>\n * </>\n * )\n * }\n * ```\n */\n\"use client\";\n\nimport { useEffect } from \"react\";\n\nexport interface RybbitAnalyticsProps {\n /**\n * Rybbit site ID (obtained from the Rybbit dashboard)\n */\n siteId: string;\n\n /**\n * Rybbit script URL (defaults to the self-hosted instance)\n * @default \"https://analytics.burdenoff.com/api/script.js\"\n */\n scriptUrl?: string;\n\n /**\n * Whether to enable tracking (useful for disabling in development)\n * @default true\n */\n enabled?: boolean;\n}\n\n/**\n * RybbitAnalytics - Embeds the Rybbit analytics tracking script.\n *\n * The script is loaded asynchronously with `defer` to avoid blocking page load.\n * It automatically tracks page views and user interactions.\n *\n * In development mode (when `enabled` is false), no script is loaded.\n */\nexport function RybbitAnalytics({\n siteId,\n scriptUrl = \"https://analytics.burdenoff.com/api/script.js\",\n enabled = true,\n}: RybbitAnalyticsProps) {\n useEffect(() => {\n if (!enabled || !siteId) return;\n\n // Check if script already exists\n const existingScript = document.querySelector(\n `script[data-site-id=\"${siteId}\"]`,\n );\n if (existingScript) return;\n\n // Create and inject the script. `crossOrigin=\"anonymous\"` puts the\n // request in CORS-no-credentials mode so the browser does not include\n // any third-party cookies the visitor may have on the analytics origin\n // (e.g. `app.rybbit.io`'s own dashboard session cookie). Without this,\n // Lighthouse `third-party-cookies` audit flags the page even though we\n // never read those cookies ourselves.\n const script = document.createElement(\"script\");\n script.src = scriptUrl;\n script.defer = true;\n script.crossOrigin = \"anonymous\";\n script.setAttribute(\"data-site-id\", siteId);\n document.head.appendChild(script);\n\n // Cleanup on unmount\n return () => {\n const scriptToRemove = document.querySelector(\n `script[data-site-id=\"${siteId}\"]`,\n );\n if (scriptToRemove) {\n document.head.removeChild(scriptToRemove);\n }\n };\n }, [siteId, scriptUrl, enabled]);\n\n // This component renders nothing - it only manages the script tag\n return null;\n}\n"]}
1
+ {"version":3,"sources":["../../src/analytics/rybbit.tsx","../../src/analytics/events.ts"],"names":["useEffect"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Rybbit Analytics Component\n *\n * Injects the Rybbit tracking script into the page head.\n * Works with both Vite (CSR) and Next.js (SSR/CSR) applications.\n *\n * @example\n * ```tsx\n * // In your app layout or root component\n * import { RybbitAnalytics } from '@burdenoff/website-sdk/analytics'\n *\n * function App() {\n * return (\n * <>\n * <RybbitAnalytics siteId=\"1\" />\n * <Router>...</Router>\n * </>\n * )\n * }\n * ```\n */\n\"use client\";\n\nimport { useEffect } from \"react\";\n\nexport interface RybbitAnalyticsProps {\n /**\n * Rybbit site ID (obtained from the Rybbit dashboard)\n */\n siteId: string;\n\n /**\n * Rybbit script URL (defaults to the self-hosted instance)\n * @default \"https://analytics.burdenoff.com/api/script.js\"\n */\n scriptUrl?: string;\n\n /**\n * Whether to enable tracking (useful for disabling in development)\n * @default true\n */\n enabled?: boolean;\n}\n\n/**\n * RybbitAnalytics - Embeds the Rybbit analytics tracking script.\n *\n * The script is loaded asynchronously with `defer` to avoid blocking page load.\n * It automatically tracks page views and user interactions.\n *\n * In development mode (when `enabled` is false), no script is loaded.\n */\nexport function RybbitAnalytics({\n siteId,\n scriptUrl = \"https://analytics.burdenoff.com/api/script.js\",\n enabled = true,\n}: RybbitAnalyticsProps) {\n useEffect(() => {\n if (!enabled || !siteId) return;\n\n // Check if script already exists\n const existingScript = document.querySelector(\n `script[data-site-id=\"${siteId}\"]`,\n );\n if (existingScript) return;\n\n // Create and inject the script. `crossOrigin=\"anonymous\"` puts the\n // request in CORS-no-credentials mode so the browser does not include\n // any third-party cookies the visitor may have on the analytics origin\n // (e.g. `app.rybbit.io`'s own dashboard session cookie). Without this,\n // Lighthouse `third-party-cookies` audit flags the page even though we\n // never read those cookies ourselves.\n const script = document.createElement(\"script\");\n script.src = scriptUrl;\n script.defer = true;\n script.crossOrigin = \"anonymous\";\n script.setAttribute(\"data-site-id\", siteId);\n document.head.appendChild(script);\n\n // Cleanup on unmount\n return () => {\n const scriptToRemove = document.querySelector(\n `script[data-site-id=\"${siteId}\"]`,\n );\n if (scriptToRemove) {\n document.head.removeChild(scriptToRemove);\n }\n };\n }, [siteId, scriptUrl, enabled]);\n\n // This component renders nothing - it only manages the script tag\n return null;\n}\n","/**\n * Website analytics events (Rybbit custom events).\n *\n * Product websites load the Rybbit script, which records pageviews on its own.\n * Funnel facts that are not a pageview (for example a successful waitlist\n * signup) are sent through `trackWebsiteEvent`, which calls `window.rybbit.event`\n * when the script is present and does nothing otherwise.\n *\n * Privacy rules (BOFF-3167):\n * - Event names come from the closed `WebsiteEventName` union.\n * - Properties are short, low-cardinality strings, numbers or booleans. Strings\n * longer than `MAX_PROPERTY_LENGTH`, values that look like an email address,\n * and any other value type are dropped, so a caller cannot leak an email,\n * name or free text into the analytics provider by mistake.\n * - Tracking never throws: analytics must not break a signup.\n */\n\n/** Custom events product websites emit. */\nexport type WebsiteEventName = \"waitlist.signed_up\";\n\nexport type WebsiteEventPropertyValue = string | number | boolean;\n\nexport type WebsiteEventProperties = Record<string, WebsiteEventPropertyValue>;\n\ndeclare global {\n interface Window {\n /** Set by the Rybbit tracking script (https://app.rybbit.io/api/script.js). */\n rybbit?: {\n event?: (\n name: string,\n properties?: Record<string, WebsiteEventPropertyValue>,\n ) => void;\n };\n }\n}\n\n/** Longest string property value that is forwarded. */\nexport const MAX_PROPERTY_LENGTH = 64;\n\nconst EMAIL_LIKE = /[^\\s@]+@[^\\s@]+/;\n\n/**\n * Keeps only short, non-identifying primitive values. Exported for tests.\n */\nexport function sanitizeWebsiteEventProperties(\n properties: WebsiteEventProperties | undefined,\n): WebsiteEventProperties {\n const safe: WebsiteEventProperties = {};\n if (!properties) return safe;\n for (const [key, value] of Object.entries(properties)) {\n if (typeof value === \"boolean\") {\n safe[key] = value;\n } else if (typeof value === \"number\") {\n if (Number.isFinite(value)) safe[key] = value;\n } else if (typeof value === \"string\") {\n const trimmed = value.trim();\n if (\n trimmed.length > 0 &&\n trimmed.length <= MAX_PROPERTY_LENGTH &&\n !EMAIL_LIKE.test(trimmed)\n ) {\n safe[key] = trimmed;\n }\n }\n }\n return safe;\n}\n\n/**\n * Sends a custom event to Rybbit when the tracking script is loaded.\n * Returns true when the event was handed to Rybbit.\n */\nexport function trackWebsiteEvent(\n name: WebsiteEventName,\n properties?: WebsiteEventProperties,\n): boolean {\n if (typeof window === \"undefined\") return false;\n try {\n const rybbit = window.rybbit;\n if (typeof rybbit?.event !== \"function\") return false;\n const safe = sanitizeWebsiteEventProperties(properties);\n if (Object.keys(safe).length > 0) {\n rybbit.event(name, safe);\n } else {\n rybbit.event(name);\n }\n return true;\n } catch {\n // Analytics is best-effort and must never break the page.\n return false;\n }\n}\n"]}
@@ -29,6 +29,43 @@ function RybbitAnalytics({
29
29
  return null;
30
30
  }
31
31
 
32
- export { RybbitAnalytics };
32
+ // src/analytics/events.ts
33
+ var MAX_PROPERTY_LENGTH = 64;
34
+ var EMAIL_LIKE = /[^\s@]+@[^\s@]+/;
35
+ function sanitizeWebsiteEventProperties(properties) {
36
+ const safe = {};
37
+ if (!properties) return safe;
38
+ for (const [key, value] of Object.entries(properties)) {
39
+ if (typeof value === "boolean") {
40
+ safe[key] = value;
41
+ } else if (typeof value === "number") {
42
+ if (Number.isFinite(value)) safe[key] = value;
43
+ } else if (typeof value === "string") {
44
+ const trimmed = value.trim();
45
+ if (trimmed.length > 0 && trimmed.length <= MAX_PROPERTY_LENGTH && !EMAIL_LIKE.test(trimmed)) {
46
+ safe[key] = trimmed;
47
+ }
48
+ }
49
+ }
50
+ return safe;
51
+ }
52
+ function trackWebsiteEvent(name, properties) {
53
+ if (typeof window === "undefined") return false;
54
+ try {
55
+ const rybbit = window.rybbit;
56
+ if (typeof rybbit?.event !== "function") return false;
57
+ const safe = sanitizeWebsiteEventProperties(properties);
58
+ if (Object.keys(safe).length > 0) {
59
+ rybbit.event(name, safe);
60
+ } else {
61
+ rybbit.event(name);
62
+ }
63
+ return true;
64
+ } catch {
65
+ return false;
66
+ }
67
+ }
68
+
69
+ export { MAX_PROPERTY_LENGTH, RybbitAnalytics, sanitizeWebsiteEventProperties, trackWebsiteEvent };
33
70
  //# sourceMappingURL=index.mjs.map
34
71
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/analytics/rybbit.tsx"],"names":[],"mappings":";;AAoDO,SAAS,eAAA,CAAgB;AAAA,EAC9B,MAAA;AAAA,EACA,SAAA,GAAY,+CAAA;AAAA,EACZ,OAAA,GAAU;AACZ,CAAA,EAAyB;AACvB,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,OAAA,IAAW,CAAC,MAAA,EAAQ;AAGzB,IAAA,MAAM,iBAAiB,QAAA,CAAS,aAAA;AAAA,MAC9B,wBAAwB,MAAM,CAAA,EAAA;AAAA,KAChC;AACA,IAAA,IAAI,cAAA,EAAgB;AAQpB,IAAA,MAAM,MAAA,GAAS,QAAA,CAAS,aAAA,CAAc,QAAQ,CAAA;AAC9C,IAAA,MAAA,CAAO,GAAA,GAAM,SAAA;AACb,IAAA,MAAA,CAAO,KAAA,GAAQ,IAAA;AACf,IAAA,MAAA,CAAO,WAAA,GAAc,WAAA;AACrB,IAAA,MAAA,CAAO,YAAA,CAAa,gBAAgB,MAAM,CAAA;AAC1C,IAAA,QAAA,CAAS,IAAA,CAAK,YAAY,MAAM,CAAA;AAGhC,IAAA,OAAO,MAAM;AACX,MAAA,MAAM,iBAAiB,QAAA,CAAS,aAAA;AAAA,QAC9B,wBAAwB,MAAM,CAAA,EAAA;AAAA,OAChC;AACA,MAAA,IAAI,cAAA,EAAgB;AAClB,QAAA,QAAA,CAAS,IAAA,CAAK,YAAY,cAAc,CAAA;AAAA,MAC1C;AAAA,IACF,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,MAAA,EAAQ,SAAA,EAAW,OAAO,CAAC,CAAA;AAG/B,EAAA,OAAO,IAAA;AACT","file":"index.mjs","sourcesContent":["/**\n * Rybbit Analytics Component\n *\n * Injects the Rybbit tracking script into the page head.\n * Works with both Vite (CSR) and Next.js (SSR/CSR) applications.\n *\n * @example\n * ```tsx\n * // In your app layout or root component\n * import { RybbitAnalytics } from '@burdenoff/website-sdk/analytics'\n *\n * function App() {\n * return (\n * <>\n * <RybbitAnalytics siteId=\"1\" />\n * <Router>...</Router>\n * </>\n * )\n * }\n * ```\n */\n\"use client\";\n\nimport { useEffect } from \"react\";\n\nexport interface RybbitAnalyticsProps {\n /**\n * Rybbit site ID (obtained from the Rybbit dashboard)\n */\n siteId: string;\n\n /**\n * Rybbit script URL (defaults to the self-hosted instance)\n * @default \"https://analytics.burdenoff.com/api/script.js\"\n */\n scriptUrl?: string;\n\n /**\n * Whether to enable tracking (useful for disabling in development)\n * @default true\n */\n enabled?: boolean;\n}\n\n/**\n * RybbitAnalytics - Embeds the Rybbit analytics tracking script.\n *\n * The script is loaded asynchronously with `defer` to avoid blocking page load.\n * It automatically tracks page views and user interactions.\n *\n * In development mode (when `enabled` is false), no script is loaded.\n */\nexport function RybbitAnalytics({\n siteId,\n scriptUrl = \"https://analytics.burdenoff.com/api/script.js\",\n enabled = true,\n}: RybbitAnalyticsProps) {\n useEffect(() => {\n if (!enabled || !siteId) return;\n\n // Check if script already exists\n const existingScript = document.querySelector(\n `script[data-site-id=\"${siteId}\"]`,\n );\n if (existingScript) return;\n\n // Create and inject the script. `crossOrigin=\"anonymous\"` puts the\n // request in CORS-no-credentials mode so the browser does not include\n // any third-party cookies the visitor may have on the analytics origin\n // (e.g. `app.rybbit.io`'s own dashboard session cookie). Without this,\n // Lighthouse `third-party-cookies` audit flags the page even though we\n // never read those cookies ourselves.\n const script = document.createElement(\"script\");\n script.src = scriptUrl;\n script.defer = true;\n script.crossOrigin = \"anonymous\";\n script.setAttribute(\"data-site-id\", siteId);\n document.head.appendChild(script);\n\n // Cleanup on unmount\n return () => {\n const scriptToRemove = document.querySelector(\n `script[data-site-id=\"${siteId}\"]`,\n );\n if (scriptToRemove) {\n document.head.removeChild(scriptToRemove);\n }\n };\n }, [siteId, scriptUrl, enabled]);\n\n // This component renders nothing - it only manages the script tag\n return null;\n}\n"]}
1
+ {"version":3,"sources":["../../src/analytics/rybbit.tsx","../../src/analytics/events.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * Rybbit Analytics Component\n *\n * Injects the Rybbit tracking script into the page head.\n * Works with both Vite (CSR) and Next.js (SSR/CSR) applications.\n *\n * @example\n * ```tsx\n * // In your app layout or root component\n * import { RybbitAnalytics } from '@burdenoff/website-sdk/analytics'\n *\n * function App() {\n * return (\n * <>\n * <RybbitAnalytics siteId=\"1\" />\n * <Router>...</Router>\n * </>\n * )\n * }\n * ```\n */\n\"use client\";\n\nimport { useEffect } from \"react\";\n\nexport interface RybbitAnalyticsProps {\n /**\n * Rybbit site ID (obtained from the Rybbit dashboard)\n */\n siteId: string;\n\n /**\n * Rybbit script URL (defaults to the self-hosted instance)\n * @default \"https://analytics.burdenoff.com/api/script.js\"\n */\n scriptUrl?: string;\n\n /**\n * Whether to enable tracking (useful for disabling in development)\n * @default true\n */\n enabled?: boolean;\n}\n\n/**\n * RybbitAnalytics - Embeds the Rybbit analytics tracking script.\n *\n * The script is loaded asynchronously with `defer` to avoid blocking page load.\n * It automatically tracks page views and user interactions.\n *\n * In development mode (when `enabled` is false), no script is loaded.\n */\nexport function RybbitAnalytics({\n siteId,\n scriptUrl = \"https://analytics.burdenoff.com/api/script.js\",\n enabled = true,\n}: RybbitAnalyticsProps) {\n useEffect(() => {\n if (!enabled || !siteId) return;\n\n // Check if script already exists\n const existingScript = document.querySelector(\n `script[data-site-id=\"${siteId}\"]`,\n );\n if (existingScript) return;\n\n // Create and inject the script. `crossOrigin=\"anonymous\"` puts the\n // request in CORS-no-credentials mode so the browser does not include\n // any third-party cookies the visitor may have on the analytics origin\n // (e.g. `app.rybbit.io`'s own dashboard session cookie). Without this,\n // Lighthouse `third-party-cookies` audit flags the page even though we\n // never read those cookies ourselves.\n const script = document.createElement(\"script\");\n script.src = scriptUrl;\n script.defer = true;\n script.crossOrigin = \"anonymous\";\n script.setAttribute(\"data-site-id\", siteId);\n document.head.appendChild(script);\n\n // Cleanup on unmount\n return () => {\n const scriptToRemove = document.querySelector(\n `script[data-site-id=\"${siteId}\"]`,\n );\n if (scriptToRemove) {\n document.head.removeChild(scriptToRemove);\n }\n };\n }, [siteId, scriptUrl, enabled]);\n\n // This component renders nothing - it only manages the script tag\n return null;\n}\n","/**\n * Website analytics events (Rybbit custom events).\n *\n * Product websites load the Rybbit script, which records pageviews on its own.\n * Funnel facts that are not a pageview (for example a successful waitlist\n * signup) are sent through `trackWebsiteEvent`, which calls `window.rybbit.event`\n * when the script is present and does nothing otherwise.\n *\n * Privacy rules (BOFF-3167):\n * - Event names come from the closed `WebsiteEventName` union.\n * - Properties are short, low-cardinality strings, numbers or booleans. Strings\n * longer than `MAX_PROPERTY_LENGTH`, values that look like an email address,\n * and any other value type are dropped, so a caller cannot leak an email,\n * name or free text into the analytics provider by mistake.\n * - Tracking never throws: analytics must not break a signup.\n */\n\n/** Custom events product websites emit. */\nexport type WebsiteEventName = \"waitlist.signed_up\";\n\nexport type WebsiteEventPropertyValue = string | number | boolean;\n\nexport type WebsiteEventProperties = Record<string, WebsiteEventPropertyValue>;\n\ndeclare global {\n interface Window {\n /** Set by the Rybbit tracking script (https://app.rybbit.io/api/script.js). */\n rybbit?: {\n event?: (\n name: string,\n properties?: Record<string, WebsiteEventPropertyValue>,\n ) => void;\n };\n }\n}\n\n/** Longest string property value that is forwarded. */\nexport const MAX_PROPERTY_LENGTH = 64;\n\nconst EMAIL_LIKE = /[^\\s@]+@[^\\s@]+/;\n\n/**\n * Keeps only short, non-identifying primitive values. Exported for tests.\n */\nexport function sanitizeWebsiteEventProperties(\n properties: WebsiteEventProperties | undefined,\n): WebsiteEventProperties {\n const safe: WebsiteEventProperties = {};\n if (!properties) return safe;\n for (const [key, value] of Object.entries(properties)) {\n if (typeof value === \"boolean\") {\n safe[key] = value;\n } else if (typeof value === \"number\") {\n if (Number.isFinite(value)) safe[key] = value;\n } else if (typeof value === \"string\") {\n const trimmed = value.trim();\n if (\n trimmed.length > 0 &&\n trimmed.length <= MAX_PROPERTY_LENGTH &&\n !EMAIL_LIKE.test(trimmed)\n ) {\n safe[key] = trimmed;\n }\n }\n }\n return safe;\n}\n\n/**\n * Sends a custom event to Rybbit when the tracking script is loaded.\n * Returns true when the event was handed to Rybbit.\n */\nexport function trackWebsiteEvent(\n name: WebsiteEventName,\n properties?: WebsiteEventProperties,\n): boolean {\n if (typeof window === \"undefined\") return false;\n try {\n const rybbit = window.rybbit;\n if (typeof rybbit?.event !== \"function\") return false;\n const safe = sanitizeWebsiteEventProperties(properties);\n if (Object.keys(safe).length > 0) {\n rybbit.event(name, safe);\n } else {\n rybbit.event(name);\n }\n return true;\n } catch {\n // Analytics is best-effort and must never break the page.\n return false;\n }\n}\n"]}
@@ -269,7 +269,7 @@ var EN = {
269
269
  timeLeft: "Time left",
270
270
  stop: "Stop",
271
271
  tapForSound: "Tap to turn on sound",
272
- finished: "The avatar has finished.",
272
+ finished: "The live avatar has ended.",
273
273
  stopped: "You stopped the avatar.",
274
274
  failed: "The live avatar couldn't start.",
275
275
  interrupted: "The live avatar stopped early.",
@@ -302,7 +302,7 @@ var ZH = {
302
302
  timeLeft: "\u5269\u4F59\u65F6\u95F4",
303
303
  stop: "\u505C\u6B62",
304
304
  tapForSound: "\u70B9\u6309\u4EE5\u5F00\u542F\u58F0\u97F3",
305
- finished: "\u865A\u62DF\u5F62\u8C61\u5DF2\u8BB2\u5B8C\u3002",
305
+ finished: "\u5B9E\u65F6\u865A\u62DF\u5F62\u8C61\u5DF2\u7ED3\u675F\u3002",
306
306
  stopped: "\u60A8\u5DF2\u505C\u6B62\u865A\u62DF\u5F62\u8C61\u3002",
307
307
  failed: "\u5B9E\u65F6\u865A\u62DF\u5F62\u8C61\u65E0\u6CD5\u542F\u52A8\u3002",
308
308
  interrupted: "\u5B9E\u65F6\u865A\u62DF\u5F62\u8C61\u63D0\u524D\u505C\u6B62\u4E86\u3002",
@@ -335,7 +335,7 @@ var HI = {
335
335
  timeLeft: "\u092C\u091A\u093E \u0939\u0941\u0906 \u0938\u092E\u092F",
336
336
  stop: "\u0930\u094B\u0915\u0947\u0902",
337
337
  tapForSound: "\u0906\u0935\u093E\u091C\u093C \u091A\u093E\u0932\u0942 \u0915\u0930\u0928\u0947 \u0915\u0947 \u0932\u093F\u090F \u091F\u0948\u092A \u0915\u0930\u0947\u0902",
338
- finished: "\u0905\u0935\u0924\u093E\u0930 \u0928\u0947 \u092A\u0922\u093C\u0928\u093E \u092A\u0942\u0930\u093E \u0915\u0930 \u0932\u093F\u092F\u093E \u0939\u0948\u0964",
338
+ finished: "\u0932\u093E\u0907\u0935 \u0905\u0935\u0924\u093E\u0930 \u0938\u092E\u093E\u092A\u094D\u0924 \u0939\u094B \u0917\u092F\u093E \u0939\u0948\u0964",
339
339
  stopped: "\u0906\u092A\u0928\u0947 \u0905\u0935\u0924\u093E\u0930 \u0915\u094B \u0930\u094B\u0915 \u0926\u093F\u092F\u093E\u0964",
340
340
  failed: "\u0932\u093E\u0907\u0935 \u0905\u0935\u0924\u093E\u0930 \u0936\u0941\u0930\u0942 \u0928\u0939\u0940\u0902 \u0939\u094B \u0938\u0915\u093E\u0964",
341
341
  interrupted: "\u0932\u093E\u0907\u0935 \u0905\u0935\u0924\u093E\u0930 \u092C\u0940\u091A \u092E\u0947\u0902 \u0939\u0940 \u0930\u0941\u0915 \u0917\u092F\u093E\u0964",
@@ -368,7 +368,7 @@ var ES = {
368
368
  timeLeft: "Tiempo restante",
369
369
  stop: "Detener",
370
370
  tapForSound: "Toca para activar el sonido",
371
- finished: "El avatar ha terminado.",
371
+ finished: "El avatar en vivo ha terminado.",
372
372
  stopped: "Has detenido el avatar.",
373
373
  failed: "No se pudo iniciar el avatar en vivo.",
374
374
  interrupted: "El avatar en vivo se detuvo antes de tiempo.",
@@ -401,7 +401,7 @@ var AR = {
401
401
  timeLeft: "\u0627\u0644\u0648\u0642\u062A \u0627\u0644\u0645\u062A\u0628\u0642\u064A",
402
402
  stop: "\u0625\u064A\u0642\u0627\u0641",
403
403
  tapForSound: "\u0627\u0646\u0642\u0631 \u0644\u062A\u0634\u063A\u064A\u0644 \u0627\u0644\u0635\u0648\u062A",
404
- finished: "\u0627\u0646\u062A\u0647\u0649 \u0627\u0644\u0623\u0641\u0627\u062A\u0627\u0631.",
404
+ finished: "\u0627\u0646\u062A\u0647\u0649 \u0627\u0644\u0623\u0641\u0627\u062A\u0627\u0631 \u0627\u0644\u0645\u0628\u0627\u0634\u0631.",
405
405
  stopped: "\u0644\u0642\u062F \u0623\u0648\u0642\u0641\u062A \u0627\u0644\u0623\u0641\u0627\u062A\u0627\u0631.",
406
406
  failed: "\u062A\u0639\u0630\u0651\u0631 \u0628\u062F\u0621 \u0627\u0644\u0623\u0641\u0627\u062A\u0627\u0631 \u0627\u0644\u0645\u0628\u0627\u0634\u0631.",
407
407
  interrupted: "\u062A\u0648\u0642\u0641 \u0627\u0644\u0623\u0641\u0627\u062A\u0627\u0631 \u0627\u0644\u0645\u0628\u0627\u0634\u0631 \u0642\u0628\u0644 \u0623\u0646 \u064A\u0646\u062A\u0647\u064A.",
@@ -434,7 +434,7 @@ var TA = {
434
434
  timeLeft: "\u0BAE\u0BC0\u0BA4\u0BAE\u0BC1\u0BB3\u0BCD\u0BB3 \u0BA8\u0BC7\u0BB0\u0BAE\u0BCD",
435
435
  stop: "\u0BA8\u0BBF\u0BB1\u0BC1\u0BA4\u0BCD\u0BA4\u0BC1",
436
436
  tapForSound: "\u0B92\u0BB2\u0BBF\u0BAF\u0BC8 \u0B87\u0BAF\u0B95\u0BCD\u0B95\u0BA4\u0BCD \u0BA4\u0B9F\u0BCD\u0B9F\u0BB5\u0BC1\u0BAE\u0BCD",
437
- finished: "\u0B85\u0BB5\u0BA4\u0BBE\u0BB0\u0BCD \u0BAA\u0B9F\u0BBF\u0BA4\u0BCD\u0BA4\u0BC1 \u0BAE\u0BC1\u0B9F\u0BBF\u0BA4\u0BCD\u0BA4\u0BA4\u0BC1.",
437
+ finished: "\u0BA8\u0BC7\u0BB0\u0BB2\u0BC8 \u0B85\u0BB5\u0BA4\u0BBE\u0BB0\u0BCD \u0BAE\u0BC1\u0B9F\u0BBF\u0BB5\u0B9F\u0BC8\u0BA8\u0BCD\u0BA4\u0BA4\u0BC1.",
438
438
  stopped: "\u0BA8\u0BC0\u0B99\u0BCD\u0B95\u0BB3\u0BCD \u0B85\u0BB5\u0BA4\u0BBE\u0BB0\u0BC8 \u0BA8\u0BBF\u0BB1\u0BC1\u0BA4\u0BCD\u0BA4\u0BBF\u0BA9\u0BC0\u0BB0\u0BCD\u0B95\u0BB3\u0BCD.",
439
439
  failed: "\u0BA8\u0BC7\u0BB0\u0BB2\u0BC8 \u0B85\u0BB5\u0BA4\u0BBE\u0BB0\u0BC8\u0BA4\u0BCD \u0BA4\u0BCA\u0B9F\u0B99\u0BCD\u0B95 \u0BAE\u0BC1\u0B9F\u0BBF\u0BAF\u0BB5\u0BBF\u0BB2\u0BCD\u0BB2\u0BC8.",
440
440
  interrupted: "\u0BA8\u0BC7\u0BB0\u0BB2\u0BC8 \u0B85\u0BB5\u0BA4\u0BBE\u0BB0\u0BCD \u0B87\u0B9F\u0BC8\u0BAF\u0BBF\u0BB2\u0BC7\u0BAF\u0BC7 \u0BA8\u0BBF\u0BA9\u0BCD\u0BB1\u0BC1\u0BB5\u0BBF\u0B9F\u0BCD\u0B9F\u0BA4\u0BC1.",
@@ -472,6 +472,20 @@ var CONSENT = /* @__PURE__ */ new Map([
472
472
  privacyLink: "How we handle this"
473
473
  }
474
474
  }
475
+ ],
476
+ [
477
+ // The voice is no longer a stored file: the server holds it in memory and plays it into the LiveKit room for Protoface.
478
+ "web-consent-2026-10-31",
479
+ {
480
+ en: {
481
+ title: "Live AI avatar (beta)",
482
+ introProduct: "This is an AI avatar, not a real person. It reads the assistant's first eligible answer to a suggested {product} question aloud for up to a minute. The answer is AI-generated and can be wrong.",
483
+ intro: "This is an AI avatar, not a real person. It reads the assistant's first eligible answer to a suggested question aloud for up to a minute. The answer is AI-generated and can be wrong.",
484
+ processing: "When you start, Cloudflare checks for bots. The answer text goes to Google to create the voice. Our server sends that voice through LiveKit to Protoface, which makes the avatar video streamed through LiveKit. Your browser connects to LiveKit, which sees your IP address.",
485
+ storage: "Your microphone and camera are not used. We do not record the live session or store the generated voice. We keep a record of this consent. The privacy notice explains our safeguards, storage periods and the providers' processing. For people aged eighteen and over.",
486
+ privacyLink: "How we handle this"
487
+ }
488
+ }
475
489
  ]
476
490
  ]);
477
491
  function avatarConsentCopy(version, locale) {