@burdenoff/website-sdk 2026.1031.2 → 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/dist/analytics/index.d.mts +41 -1
- package/dist/analytics/index.d.ts +41 -1
- package/dist/analytics/index.js +40 -0
- package/dist/analytics/index.js.map +1 -1
- package/dist/analytics/index.mjs +38 -1
- package/dist/analytics/index.mjs.map +1 -1
- package/dist/components/waitlist.js +39 -0
- package/dist/components/waitlist.js.map +1 -1
- package/dist/components/waitlist.mjs +39 -0
- package/dist/components/waitlist.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +40 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -24,4 +24,44 @@ interface RybbitAnalyticsProps {
|
|
|
24
24
|
*/
|
|
25
25
|
declare function RybbitAnalytics({ siteId, scriptUrl, enabled, }: RybbitAnalyticsProps): null;
|
|
26
26
|
|
|
27
|
-
|
|
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
|
-
|
|
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 };
|
package/dist/analytics/index.js
CHANGED
|
@@ -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"]}
|
package/dist/analytics/index.mjs
CHANGED
|
@@ -29,6 +29,43 @@ function RybbitAnalytics({
|
|
|
29
29
|
return null;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
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"]}
|
|
@@ -38,6 +38,43 @@ var __defProp = Object.defineProperty;
|
|
|
38
38
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
39
39
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
40
40
|
|
|
41
|
+
// src/analytics/events.ts
|
|
42
|
+
var MAX_PROPERTY_LENGTH = 64;
|
|
43
|
+
var EMAIL_LIKE = /[^\s@]+@[^\s@]+/;
|
|
44
|
+
function sanitizeWebsiteEventProperties(properties) {
|
|
45
|
+
const safe = {};
|
|
46
|
+
if (!properties) return safe;
|
|
47
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
48
|
+
if (typeof value === "boolean") {
|
|
49
|
+
safe[key] = value;
|
|
50
|
+
} else if (typeof value === "number") {
|
|
51
|
+
if (Number.isFinite(value)) safe[key] = value;
|
|
52
|
+
} else if (typeof value === "string") {
|
|
53
|
+
const trimmed = value.trim();
|
|
54
|
+
if (trimmed.length > 0 && trimmed.length <= MAX_PROPERTY_LENGTH && !EMAIL_LIKE.test(trimmed)) {
|
|
55
|
+
safe[key] = trimmed;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return safe;
|
|
60
|
+
}
|
|
61
|
+
function trackWebsiteEvent(name, properties) {
|
|
62
|
+
if (typeof window === "undefined") return false;
|
|
63
|
+
try {
|
|
64
|
+
const rybbit = window.rybbit;
|
|
65
|
+
if (typeof rybbit?.event !== "function") return false;
|
|
66
|
+
const safe = sanitizeWebsiteEventProperties(properties);
|
|
67
|
+
if (Object.keys(safe).length > 0) {
|
|
68
|
+
rybbit.event(name, safe);
|
|
69
|
+
} else {
|
|
70
|
+
rybbit.event(name);
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
41
78
|
// src/client/security.ts
|
|
42
79
|
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
|
|
43
80
|
var RESERVED_HEADERS = /* @__PURE__ */ new Set([
|
|
@@ -709,6 +746,7 @@ function WaitlistPage({
|
|
|
709
746
|
product?.id || config.productId
|
|
710
747
|
);
|
|
711
748
|
if (result.success) {
|
|
749
|
+
trackWebsiteEvent("waitlist.signed_up", { form: "page" });
|
|
712
750
|
sonner.toast.success(result.message ?? successMessage);
|
|
713
751
|
reset();
|
|
714
752
|
} else {
|
|
@@ -859,6 +897,7 @@ function WaitlistForm({
|
|
|
859
897
|
product?.id || config.productId
|
|
860
898
|
);
|
|
861
899
|
if (result.success) {
|
|
900
|
+
trackWebsiteEvent("waitlist.signed_up", { form: "inline" });
|
|
862
901
|
sonner.toast.success(result.message ?? "You have been added to the waitlist!");
|
|
863
902
|
setEmail("");
|
|
864
903
|
setName("");
|