@tekibo/feedpulse-sdk 0.7.3 → 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.
Files changed (38) hide show
  1. package/README.md +83 -83
  2. package/dist/core/cookies.d.ts +37 -0
  3. package/dist/core/struggle-detector.d.ts +33 -0
  4. package/dist/core/tracker.d.ts +38 -0
  5. package/dist/core/types.d.ts +59 -0
  6. package/dist/index.d.ts +3 -0
  7. package/dist/index.js +1 -1
  8. package/dist/index.mjs +127 -176
  9. package/dist/nuxt/BotPanel.vue.d.ts +17 -0
  10. package/dist/nuxt/FeedPulseBot.vue.d.ts +24 -0
  11. package/dist/nuxt/FeedPulseProvider.vue.d.ts +21 -0
  12. package/dist/nuxt/FeedPulseWidget.vue.d.ts +10 -0
  13. package/dist/nuxt/composables.d.ts +4 -0
  14. package/dist/nuxt/consent-banner.vue.d.ts +16 -0
  15. package/dist/nuxt/index.d.ts +5 -0
  16. package/dist/nuxt.d.ts +2 -0
  17. package/dist/nuxt.js +2 -2
  18. package/dist/nuxt.mjs +283 -336
  19. package/dist/react/FeedPulseBot.d.ts +21 -0
  20. package/dist/react/FeedPulseProvider.d.ts +13 -0
  21. package/dist/react/FeedPulseWidget.d.ts +8 -0
  22. package/dist/react/consent-banner.d.ts +11 -0
  23. package/dist/react/hooks.d.ts +1 -0
  24. package/dist/react/index.d.ts +4 -0
  25. package/dist/react.d.ts +2 -0
  26. package/dist/react.js +3 -3
  27. package/dist/react.mjs +275 -315
  28. package/dist/struggle-detector-CT3YShuG.js +1 -0
  29. package/dist/{struggle-detector-BqTHgw95.mjs → struggle-detector-Cwe3PD9s.mjs} +40 -25
  30. package/package.json +4 -11
  31. package/templates/nextjs-proxy.ts +36 -36
  32. package/templates/nuxt-bot-proxy.ts +60 -93
  33. package/templates/nuxt-proxy.ts +44 -44
  34. package/bin/feedpulse-bot-build.mjs +0 -121
  35. package/dist/server.js +0 -1
  36. package/dist/server.mjs +0 -18
  37. package/dist/struggle-detector-DV52qDLp.js +0 -1
  38. package/templates/nuxt-config-id-route.ts +0 -15
package/README.md CHANGED
@@ -1,83 +1,83 @@
1
- # @tekibo/feedpulse-sdk
2
-
3
- FeedPulse analytics and feedback SDK for Nuxt and React.
4
-
5
- ## Setup
6
-
7
- ### 1) Create a proxy route in your own app
8
-
9
- - Nuxt: copy `templates/nuxt-proxy.ts` to `server/api/fp-proxy.post.ts`
10
- - Next.js App Router: copy `templates/nextjs-proxy.ts` to `app/api/fp-proxy/route.ts`
11
-
12
- ### 2) Set server-only environment variables
13
-
14
- ```env
15
- FEEDPULSE_API_KEY=fp_live_your_key
16
- FEEDPULSE_WORKER_URL=https://feedpulse-workers.tekibohelp.workers.dev/ingest
17
- ```
18
-
19
- Never use `NUXT_PUBLIC_` or `NEXT_PUBLIC_` for the FeedPulse API key.
20
-
21
- ### 3) Use SDK without exposing key in client
22
-
23
- ```vue
24
- <script setup lang="ts">
25
- import { FeedPulseProvider, FeedPulseWidget } from "@tekibo/feedpulse-sdk/nuxt";
26
- </script>
27
-
28
- <template>
29
- <FeedPulseProvider :debug="true">
30
- <NuxtPage />
31
- <FeedPulseWidget position="bottom-right" />
32
- </FeedPulseProvider>
33
- </template>
34
- ```
35
-
36
- ```tsx
37
- import { FeedPulseProvider, FeedPulseWidget } from "@tekibo/feedpulse-sdk/react";
38
-
39
- export default function RootLayout({ children }: { children: React.ReactNode }) {
40
- return (
41
- <FeedPulseProvider debug>
42
- {children}
43
- <FeedPulseWidget position="bottom-right" />
44
- </FeedPulseProvider>
45
- );
46
- }
47
- ```
48
-
49
- ## How it works
50
-
51
- - Browser sends events to `/api/fp-proxy` by default
52
- - Your server route injects `FEEDPULSE_API_KEY`
53
- - Server forwards to FeedPulse worker ingest endpoint
54
- - API key never appears in browser source or client payload
55
-
56
- ## Provider props
57
-
58
- - `proxyEndpoint?: string` default `/api/fp-proxy`
59
- - `debug?: boolean`
60
- - `consentRequired?: boolean` default `true`
61
-
62
- ## Tracker config
63
-
64
- ```ts
65
- type FeedPulseConfig = {
66
- proxyEndpoint?: string;
67
- batchInterval?: number;
68
- debug?: boolean;
69
- };
70
- ```
71
-
72
- ## What is tracked
73
-
74
- - `click`
75
- - `hover`
76
- - `scroll_into_view`
77
- - `feedback`
78
- - `consent`
79
-
80
- ## Templates included in package
81
-
82
- - `templates/nuxt-proxy.ts`
83
- - `templates/nextjs-proxy.ts`
1
+ # @tekibo/feedpulse-sdk
2
+
3
+ FeedPulse analytics and feedback SDK for Nuxt and React.
4
+
5
+ ## Setup
6
+
7
+ ### 1) Create a proxy route in your own app
8
+
9
+ - Nuxt: copy `templates/nuxt-proxy.ts` to `server/api/fp-proxy.post.ts`
10
+ - Next.js App Router: copy `templates/nextjs-proxy.ts` to `app/api/fp-proxy/route.ts`
11
+
12
+ ### 2) Set server-only environment variables
13
+
14
+ ```env
15
+ FEEDPULSE_API_KEY=fp_live_your_key
16
+ FEEDPULSE_WORKER_URL=https://feedpulse-workers.tekibohelp.workers.dev/ingest
17
+ ```
18
+
19
+ Never use `NUXT_PUBLIC_` or `NEXT_PUBLIC_` for the FeedPulse API key.
20
+
21
+ ### 3) Use SDK without exposing key in client
22
+
23
+ ```vue
24
+ <script setup lang="ts">
25
+ import { FeedPulseProvider, FeedPulseWidget } from "@tekibo/feedpulse-sdk/nuxt";
26
+ </script>
27
+
28
+ <template>
29
+ <FeedPulseProvider :debug="true">
30
+ <NuxtPage />
31
+ <FeedPulseWidget position="bottom-right" />
32
+ </FeedPulseProvider>
33
+ </template>
34
+ ```
35
+
36
+ ```tsx
37
+ import { FeedPulseProvider, FeedPulseWidget } from "@tekibo/feedpulse-sdk/react";
38
+
39
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
40
+ return (
41
+ <FeedPulseProvider debug>
42
+ {children}
43
+ <FeedPulseWidget position="bottom-right" />
44
+ </FeedPulseProvider>
45
+ );
46
+ }
47
+ ```
48
+
49
+ ## How it works
50
+
51
+ - Browser sends events to `/api/fp-proxy` by default
52
+ - Your server route injects `FEEDPULSE_API_KEY`
53
+ - Server forwards to FeedPulse worker ingest endpoint
54
+ - API key never appears in browser source or client payload
55
+
56
+ ## Provider props
57
+
58
+ - `proxyEndpoint?: string` default `/api/fp-proxy`
59
+ - `debug?: boolean`
60
+ - `consentRequired?: boolean` default `true`
61
+
62
+ ## Tracker config
63
+
64
+ ```ts
65
+ type FeedPulseConfig = {
66
+ proxyEndpoint?: string;
67
+ batchInterval?: number;
68
+ debug?: boolean;
69
+ };
70
+ ```
71
+
72
+ ## What is tracked
73
+
74
+ - `click`
75
+ - `hover`
76
+ - `scroll_into_view`
77
+ - `feedback`
78
+ - `consent`
79
+
80
+ ## Templates included in package
81
+
82
+ - `templates/nuxt-proxy.ts`
83
+ - `templates/nextjs-proxy.ts`
@@ -0,0 +1,37 @@
1
+ export declare const COOKIE_KEYS: {
2
+ readonly USER_ID: "__fp_uid";
3
+ readonly SESSION_ID: "__fp_sid";
4
+ readonly SESSION_START: "__fp_ss";
5
+ readonly CONSENT: "__fp_consent";
6
+ readonly UTM: "__fp_utm";
7
+ };
8
+ export type ConsentState = {
9
+ necessary: true;
10
+ analytics: boolean;
11
+ marketing: boolean;
12
+ version: string;
13
+ timestamp: number;
14
+ };
15
+ export type CookieData = {
16
+ userId: string | null;
17
+ sessionId: string;
18
+ sessionStart: number;
19
+ consent: ConsentState | null;
20
+ utm: Record<string, string> | null;
21
+ isReturningUser: boolean;
22
+ };
23
+ export declare class CookieManager {
24
+ private transientSession;
25
+ initSession(): {
26
+ sessionId: string;
27
+ sessionStart: number;
28
+ isReturningUser: boolean;
29
+ };
30
+ initUser(): string;
31
+ getConsent(): ConsentState | null;
32
+ setConsent(state: Omit<ConsentState, "necessary" | "timestamp">): ConsentState;
33
+ clearAnalyticsCookies(): void;
34
+ isConsentExpired(): boolean;
35
+ captureUTM(): Record<string, string> | null;
36
+ getAllData(analyticsConsented: boolean): CookieData;
37
+ }
@@ -0,0 +1,33 @@
1
+ type TriggerReason = "rage_click" | "dead_click" | "idle" | "rapid_scroll" | "long_page_time" | "back_navigation" | "js_error" | "form_abandonment" | "repeated_page";
2
+ export declare class StruggleDetector {
3
+ private opts;
4
+ private score;
5
+ private triggered;
6
+ private cooldownUntil;
7
+ private reasons;
8
+ private clickMap;
9
+ private lastActivity;
10
+ private scrollPositions;
11
+ private idleTimer;
12
+ private clickHandler;
13
+ private scrollHandler;
14
+ private activityHandler;
15
+ private errorHandler;
16
+ private popstateHandler;
17
+ private formListeners;
18
+ constructor(opts: {
19
+ onTrigger: (score: number, reasons: TriggerReason[]) => void;
20
+ });
21
+ start(): void;
22
+ private onClick;
23
+ private onScroll;
24
+ private onError;
25
+ private onBackNavigation;
26
+ private onActivity;
27
+ private onFormBlur;
28
+ private checkIdle;
29
+ private addScore;
30
+ reset(): void;
31
+ stop(): void;
32
+ }
33
+ export {};
@@ -0,0 +1,38 @@
1
+ import { FeedPulseConfig, FeedPulseEvent } from './types';
2
+ import { CookieManager } from './cookies';
3
+ export declare class FeedPulseTracker {
4
+ private config;
5
+ private queue;
6
+ private flushInterval;
7
+ private sessionId;
8
+ private observer;
9
+ private intersectionObserver;
10
+ private scrollFallbackListener;
11
+ private resizeFallbackListener;
12
+ private seenInView;
13
+ private cookieManager;
14
+ private cookieData;
15
+ private analyticsConsented;
16
+ private isInitialized;
17
+ private beforeUnloadListener;
18
+ private visibilityListener;
19
+ private clickListener;
20
+ private hoverStartListener;
21
+ private hoverEndListener;
22
+ constructor(config: FeedPulseConfig);
23
+ init(): void;
24
+ private attachGlobalListeners;
25
+ private observeDOM;
26
+ track(eventType: FeedPulseEvent["eventType"], elementId: string, metadata?: Record<string, unknown>): void;
27
+ trackFeedback(elementId: string | null, rating: number | null, message: string | null): void;
28
+ private flush;
29
+ private sendImmediate;
30
+ private startFlushInterval;
31
+ updateCookieData(data: ReturnType<CookieManager["getAllData"]>): void;
32
+ private refreshCookieData;
33
+ private isBrowser;
34
+ private getDeviceType;
35
+ private getBrowser;
36
+ private getOS;
37
+ destroy(): void;
38
+ }
@@ -0,0 +1,59 @@
1
+ export type FeedPulseEventType = "click" | "hover" | "scroll_into_view" | "feedback" | "consent";
2
+ export type FeedPulseEvent = {
3
+ elementId: string;
4
+ eventType: FeedPulseEventType;
5
+ sessionId: string;
6
+ visitorId: string | null;
7
+ isReturningUser?: boolean;
8
+ sessionStart?: string | null;
9
+ utmSource?: string | null;
10
+ utmMedium?: string | null;
11
+ utmCampaign?: string | null;
12
+ utmTerm?: string | null;
13
+ utmContent?: string | null;
14
+ consentAnalytics?: boolean;
15
+ page: string;
16
+ referrer?: string;
17
+ device: string;
18
+ browser?: string;
19
+ os?: string;
20
+ metadata?: Record<string, unknown>;
21
+ timestamp: string;
22
+ };
23
+ export type FeedPulseConfig = {
24
+ proxyEndpoint?: string;
25
+ batchInterval?: number;
26
+ debug?: boolean;
27
+ };
28
+ export type BotFaqItem = {
29
+ q: string;
30
+ a: string;
31
+ };
32
+ export type BotNavigableElement = {
33
+ label: string;
34
+ description: string;
35
+ instruction: string;
36
+ };
37
+ export type BotFormField = {
38
+ label: string;
39
+ hint: string;
40
+ };
41
+ export type BotFeature = {
42
+ name: string;
43
+ description: string;
44
+ };
45
+ export type BotConfig = {
46
+ pageContext?: string;
47
+ systemPrompt?: string;
48
+ faq?: BotFaqItem[];
49
+ canNavigate?: boolean;
50
+ navigableElements?: BotNavigableElement[];
51
+ canHelpForms?: boolean;
52
+ formGuide?: BotFormField[];
53
+ features?: BotFeature[];
54
+ escalateTo?: {
55
+ label: string;
56
+ url: string;
57
+ };
58
+ initialMessage?: string;
59
+ };
@@ -0,0 +1,3 @@
1
+ export * from './core/cookies';
2
+ export { FeedPulseTracker } from './core/tracker';
3
+ export * from './core/types';
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function p(o){for(var e=1;e<arguments.length;e++){var i=arguments[e];for(var s in i)o[s]=i[s]}return o}var y={read:function(o){return o[0]==='"'&&(o=o.slice(1,-1)),o.replace(/(%[\dA-F]{2})+/gi,decodeURIComponent)},write:function(o){return encodeURIComponent(o).replace(/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g,decodeURIComponent)}};function m(o,e){function i(t,n,r){if(!(typeof document>"u")){r=p({},e,r),typeof r.expires=="number"&&(r.expires=new Date(Date.now()+r.expires*864e5)),r.expires&&(r.expires=r.expires.toUTCString()),t=encodeURIComponent(t).replace(/%(2[346B]|5E|60|7C)/g,decodeURIComponent).replace(/[()]/g,escape);var a="";for(var l in r)r[l]&&(a+="; "+l,r[l]!==!0&&(a+="="+r[l].split(";")[0]));return document.cookie=t+"="+o.write(n,t)+a}}function s(t){if(!(typeof document>"u"||arguments.length&&!t)){for(var n=document.cookie?document.cookie.split("; "):[],r={},a=0;a<n.length;a++){var l=n[a].split("="),d=l.slice(1).join("=");try{var h=decodeURIComponent(l[0]);if(r[h]=o.read(d,h),t===h)break}catch{}}return t?r[t]:r}}return Object.create({set:i,get:s,remove:function(t,n){i(t,"",p({},n,{expires:-1}))},withAttributes:function(t){return m(this.converter,p({},this.attributes,t))},withConverter:function(t){return m(p({},this.converter,t),this.attributes)}},{attributes:{value:Object.freeze(e)},converter:{value:Object.freeze(o)}})}var u=m(y,{path:"/"});const c={USER_ID:"__fp_uid",SESSION_ID:"__fp_sid",SESSION_START:"__fp_ss",CONSENT:"__fp_consent",UTM:"__fp_utm"},g=1/48,I=365,b=365,k=30;function S(){return typeof window<"u"}function v(o){if(!o)return null;try{return JSON.parse(o)}catch{return null}}class w{initSession(){const e=!!u.get(c.USER_ID);let i=u.get(c.SESSION_ID)??null,s=Number(u.get(c.SESSION_START)||Date.now());return i||(i=crypto.randomUUID(),s=Date.now()),u.set(c.SESSION_ID,i,{expires:g}),u.set(c.SESSION_START,String(s),{expires:g}),{sessionId:i,sessionStart:s,isReturningUser:e}}initUser(){let e=u.get(c.USER_ID)??null;return e||(e=crypto.randomUUID(),u.set(c.USER_ID,e,{expires:b})),e}getConsent(){return v(u.get(c.CONSENT))}setConsent(e){const i={necessary:!0,analytics:e.analytics,marketing:e.marketing,version:e.version,timestamp:Date.now()};return u.set(c.CONSENT,JSON.stringify(i),{expires:I}),i}clearAnalyticsCookies(){u.remove(c.USER_ID)}isConsentExpired(){const e=this.getConsent();if(!e)return!0;const i=365*24*60*60*1e3;return Date.now()-e.timestamp>i}captureUTM(){if(!S())return null;const e=new URLSearchParams(window.location.search),i={};for(const s of["utm_source","utm_medium","utm_campaign","utm_term","utm_content"]){const t=e.get(s);t&&(i[s]=t)}return Object.keys(i).length===0?v(u.get(c.UTM)):(u.set(c.UTM,JSON.stringify(i),{expires:k}),i)}getAllData(e){const{sessionId:i,sessionStart:s,isReturningUser:t}=this.initSession();return{userId:e?this.initUser():null,sessionId:i,sessionStart:s,consent:this.getConsent(),utm:this.captureUTM(),isReturningUser:e?t:!1}}}class _{constructor(){this.cursorEl=null}createCursor(){if(this.cursorEl)return this.cursorEl;const e=document.createElement("div");return e.id="__fp_bot_cursor",e.innerHTML='<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M5 3l14 9-7 1-4 7L5 3z" fill="#6366f1" stroke="white" stroke-width="1.5"/></svg>',Object.assign(e.style,{position:"fixed",zIndex:"999999",pointerEvents:"none",transition:"left 0.6s ease, top 0.6s ease",transform:"translate(-4px,-4px)",left:"50%",top:"50%",opacity:"0"}),document.body.appendChild(e),this.cursorEl=e,e}async moveTo(e,i){const s=this.createCursor();s.style.left=`${e}px`,s.style.top=`${i}px`,await new Promise(t=>setTimeout(t,700))}async clickAnimation(e,i){if(!document.getElementById("__fp_cursor_styles")){const t=document.createElement("style");t.id="__fp_cursor_styles",t.textContent="@keyframes fp-ripple{from{transform:scale(0);opacity:1}to{transform:scale(2);opacity:0}}",document.head.appendChild(t)}const s=document.createElement("div");Object.assign(s.style,{position:"fixed",left:`${e-16}px`,top:`${i-16}px`,width:"32px",height:"32px",borderRadius:"50%",background:"rgba(99,102,241,0.4)",zIndex:"999998",pointerEvents:"none",animation:"fp-ripple 0.4s ease-out forwards"}),document.body.appendChild(s),await new Promise(t=>setTimeout(t,400)),s.remove()}async typeText(e,i){e.focus(),(e instanceof HTMLInputElement||e instanceof HTMLTextAreaElement)&&(e.value="");for(const s of i)await new Promise(t=>setTimeout(t,60+Math.random()*40)),(e instanceof HTMLInputElement||e instanceof HTMLTextAreaElement)&&(e.value+=s,e.dispatchEvent(new Event("input",{bubbles:!0})))}async perform(e){try{const i=this.createCursor();if(i.style.opacity="1",e.type==="navigate"&&e.value)return await this.moveTo(window.innerWidth/2,window.innerHeight/2),await new Promise(d=>setTimeout(d,300)),this.destroy(),window.location.href=e.value,{success:!0,message:`Navigating to ${e.value}...`};if(!e.selector)return{success:!1,message:"No target element specified."};const t=!/[#.\[\]\s>:+~]/.test(e.selector)?`[data-fp-id="${e.selector}"], #${e.selector}, [name="${e.selector}"]`:e.selector,n=document.querySelector(t);if(!n)return{success:!1,message:`Could not find "${e.label}" on the page.`};const r=n.getBoundingClientRect(),a=r.left+r.width/2,l=r.top+r.height/2;if(await this.moveTo(a,l),e.type==="click"&&(await this.clickAnimation(a,l),n.click(),n.dispatchEvent(new MouseEvent("click",{bubbles:!0,cancelable:!0}))),e.type==="scroll"){n.scrollIntoView({behavior:"smooth",block:"center"});const d=n.style.outline,h=n.style.outlineOffset;n.style.outline="2px solid #6366f1",n.style.outlineOffset="3px",await new Promise(f=>setTimeout(f,1500)),n.style.outline=d,n.style.outlineOffset=h}if(e.type==="fill"&&e.value&&await this.typeText(n,e.value),e.type==="highlight"){n.scrollIntoView({behavior:"smooth",block:"center"});const d=n.style.outline,h=n.style.outlineOffset;n.style.outline="2px solid #6366f1",n.style.outlineOffset="3px",await new Promise(f=>setTimeout(f,1500)),n.style.outline=d,n.style.outlineOffset=h}return await new Promise(d=>setTimeout(d,400)),this.destroy(),{success:!0,message:`Done! I ${e.type}ed "${e.label}".`}}catch{return this.destroy(),{success:!1,message:"Something went wrong performing that action."}}}destroy(){this.cursorEl?.remove(),this.cursorEl=null}}class D{constructor(e){this.queue=[],this.flushInterval=null,this.sessionId="",this.observer=null,this.intersectionObserver=null,this.scrollFallbackListener=null,this.resizeFallbackListener=null,this.seenInView=new Set,this.cookieManager=new w,this.cookieData=null,this.analyticsConsented=!1,this.isInitialized=!1,this.beforeUnloadListener=null,this.visibilityListener=null,this.config={proxyEndpoint:e.proxyEndpoint??"/api/fp-proxy",batchInterval:e.batchInterval??5e3,debug:e.debug??!1}}init(){!this.isBrowser()||this.isInitialized||(this.isInitialized=!0,this.refreshCookieData(),this.startFlushInterval(),this.attachGlobalListeners(),this.observeDOM())}attachGlobalListeners(){document.addEventListener("click",s=>{const t=s.target.closest("[data-fp-id]");t&&this.track("click",t.getAttribute("data-fp-id"),{x:s.clientX,y:s.clientY})},{passive:!0});let e=0,i=null;document.addEventListener("mouseover",s=>{const t=s.target.closest("[data-fp-id]");t&&(e=Date.now(),i=t.getAttribute("data-fp-id"))},{passive:!0}),document.addEventListener("mouseout",s=>{if(!s.target.closest("[data-fp-id]")||!i)return;const n=Date.now()-e;n>500&&this.track("hover",i,{hoverDuration:n}),i=null},{passive:!0})}observeDOM(){const e=t=>{this.seenInView.has(t)||(this.seenInView.add(t),this.track("scroll_into_view",t,{scrollDepth:Math.round(window.scrollY/Math.max(1,document.body.scrollHeight)*100)}))};this.intersectionObserver=new IntersectionObserver(t=>{for(const n of t){const r=n.target.dataset.fpId;!r||!n.isIntersecting||e(r)}},{threshold:.1});const i=()=>{document.querySelectorAll("[data-fp-id]").forEach(t=>{this.intersectionObserver?.observe(t)})},s=()=>{const t=window.innerHeight*.1,n=window.innerHeight*.9;document.querySelectorAll("[data-fp-id]").forEach(r=>{const a=r.dataset.fpId;if(!a||this.seenInView.has(a))return;const l=r.getBoundingClientRect();l.bottom>=t&&l.top<=n&&e(a)})};i(),s(),this.scrollFallbackListener=()=>s(),this.resizeFallbackListener=()=>s(),window.addEventListener("scroll",this.scrollFallbackListener,{passive:!0}),window.addEventListener("resize",this.resizeFallbackListener,{passive:!0}),this.observer=new MutationObserver(()=>i()),this.observer.observe(document.body,{childList:!0,subtree:!0})}track(e,i,s){this.isBrowser()&&(this.refreshCookieData(),this.queue.push({elementId:i,eventType:e,sessionId:this.cookieData?.sessionId??this.sessionId,visitorId:this.cookieData?.userId??null,isReturningUser:this.cookieData?.isReturningUser??!1,sessionStart:this.cookieData?.sessionStart?new Date(this.cookieData.sessionStart).toISOString():null,utmSource:this.cookieData?.utm?.utm_source??null,utmMedium:this.cookieData?.utm?.utm_medium??null,utmCampaign:this.cookieData?.utm?.utm_campaign??null,utmTerm:this.cookieData?.utm?.utm_term??null,utmContent:this.cookieData?.utm?.utm_content??null,consentAnalytics:this.cookieData?.consent?.analytics??!1,page:window.location.pathname,referrer:document.referrer,device:this.getDeviceType(),browser:this.getBrowser(),os:this.getOS(),metadata:s,timestamp:new Date().toISOString()}))}trackFeedback(e,i,s){this.isBrowser()&&(!i&&!s||(this.refreshCookieData(),this.sendImmediate([{elementId:e??"__page__",eventType:"feedback",sessionId:this.cookieData?.sessionId??this.sessionId,visitorId:this.cookieData?.userId??null,isReturningUser:this.cookieData?.isReturningUser??!1,sessionStart:this.cookieData?.sessionStart?new Date(this.cookieData.sessionStart).toISOString():null,utmSource:this.cookieData?.utm?.utm_source??null,utmMedium:this.cookieData?.utm?.utm_medium??null,utmCampaign:this.cookieData?.utm?.utm_campaign??null,utmTerm:this.cookieData?.utm?.utm_term??null,utmContent:this.cookieData?.utm?.utm_content??null,consentAnalytics:this.cookieData?.consent?.analytics??!1,page:window.location.pathname,device:this.getDeviceType(),metadata:{rating:i,message:s},timestamp:new Date().toISOString()}])))}async flush(){if(this.queue.length===0)return;const e=[...this.queue];this.queue=[],await this.sendImmediate(e)}async sendImmediate(e){try{const i=await fetch(this.config.proxyEndpoint,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({events:e}),keepalive:!0});if(this.config.debug&&i.ok){const s=await i.json().catch(()=>null);s?.ingested!==void 0&&s?.pipeline&&globalThis.console.debug(`[FeedPulse] ${s.ingested} events via ${s.pipeline} pipeline`)}}catch{}}startFlushInterval(){this.flushInterval=setInterval(()=>this.flush(),this.config.batchInterval),this.beforeUnloadListener=()=>this.flush(),this.visibilityListener=()=>{document.visibilityState==="hidden"&&this.flush()},window.addEventListener("beforeunload",this.beforeUnloadListener),document.addEventListener("visibilitychange",this.visibilityListener)}updateCookieData(e){this.cookieData=e,this.sessionId=e.sessionId,this.analyticsConsented=!!e.consent?.analytics}refreshCookieData(){const e=this.cookieManager.getAllData(this.analyticsConsented);this.cookieData=e,this.sessionId=e.sessionId}isBrowser(){return typeof window<"u"&&typeof document<"u"}getDeviceType(){const e=navigator.userAgent;return/Mobi|Android/i.test(e)?"mobile":/Tablet|iPad/i.test(e)?"tablet":"desktop"}getBrowser(){const e=navigator.userAgent;return e.includes("Edg")?"Edge":e.includes("Chrome")?"Chrome":e.includes("Firefox")?"Firefox":e.includes("Safari")?"Safari":"Other"}getOS(){const e=navigator.userAgent;return e.includes("Windows")?"Windows":e.includes("Mac")?"macOS":e.includes("Linux")?"Linux":e.includes("Android")?"Android":/iPhone|iOS/.test(e)?"iOS":"Other"}destroy(){this.flushInterval&&clearInterval(this.flushInterval),this.beforeUnloadListener&&window.removeEventListener("beforeunload",this.beforeUnloadListener),this.visibilityListener&&document.removeEventListener("visibilitychange",this.visibilityListener),this.scrollFallbackListener&&window.removeEventListener("scroll",this.scrollFallbackListener),this.resizeFallbackListener&&window.removeEventListener("resize",this.resizeFallbackListener),this.observer?.disconnect(),this.intersectionObserver?.disconnect(),this.isInitialized=!1,this.flush()}}exports.COOKIE_KEYS=c;exports.CookieManager=w;exports.CursorActor=_;exports.FeedPulseTracker=D;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function h(r){for(var e=1;e<arguments.length;e++){var i=arguments[e];for(var t in i)t!=="__proto__"&&(r[t]=i[t])}return r}var S={read:function(r){return r[0]==='"'&&(r=r.slice(1,-1)),r.replace(/(%[\dA-F]{2})+/gi,decodeURIComponent)},write:function(r){return encodeURIComponent(r).replace(/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g,decodeURIComponent)}};function f(r,e){function i(n,a,s){if(!(typeof document>"u")){s=h({},e,s),typeof s.expires=="number"&&(s.expires=new Date(Date.now()+s.expires*864e5)),s.expires&&(s.expires=s.expires.toUTCString()),n=encodeURIComponent(n).replace(/%(2[346B]|5E|60|7C)/g,decodeURIComponent).replace(/[()]/g,escape);var l="";for(var u in s)s[u]&&(l+="; "+u,s[u]!==!0&&(l+="="+s[u].split(";")[0]));return document.cookie=n+"="+r.write(a,n)+l}}function t(n){if(!(typeof document>"u"||arguments.length&&!n)){for(var a=document.cookie?document.cookie.split("; "):[],s={},l=0;l<a.length;l++){var u=a[l].split("="),m=u.slice(1).join("=");try{var d=decodeURIComponent(u[0]);if(d in s||(s[d]=r.read(m,d)),n===d)break}catch{}}return n?s[n]:s}}return Object.create({set:i,get:t,remove:function(n,a){i(n,"",h({},a,{expires:-1}))},withAttributes:function(n){return f(this.converter,h({},this.attributes,n))},withConverter:function(n){return f(h({},this.converter,n),this.attributes)}},{attributes:{value:Object.freeze(e)},converter:{value:Object.freeze(r)}})}var c=f(S,{path:"/"});const o={USER_ID:"__fp_uid",SESSION_ID:"__fp_sid",SESSION_START:"__fp_ss",CONSENT:"__fp_consent",UTM:"__fp_utm"},p=1/48,I=365,k=365,w=30;function D(){return typeof window<"u"}function v(r){if(!r)return null;try{return JSON.parse(r)}catch{return null}}class g{transientSession=null;initSession(){const e=!!c.get(o.USER_ID);let i=c.get(o.SESSION_ID)??null,t=Number(c.get(o.SESSION_START)||Date.now());return i||(i=crypto.randomUUID(),t=Date.now()),c.set(o.SESSION_ID,i,{expires:p}),c.set(o.SESSION_START,String(t),{expires:p}),{sessionId:i,sessionStart:t,isReturningUser:e}}initUser(){let e=c.get(o.USER_ID)??null;return e||(e=crypto.randomUUID(),c.set(o.USER_ID,e,{expires:k})),e}getConsent(){return v(c.get(o.CONSENT))}setConsent(e){const i={necessary:!0,analytics:e.analytics,marketing:e.marketing,version:e.version,timestamp:Date.now()};return c.set(o.CONSENT,JSON.stringify(i),{expires:I}),i}clearAnalyticsCookies(){for(const e of[o.USER_ID,o.SESSION_ID,o.SESSION_START,o.UTM])c.remove(e);this.transientSession=null}isConsentExpired(){const e=this.getConsent();if(!e)return!0;const i=365*24*60*60*1e3;return Date.now()-e.timestamp>i}captureUTM(){if(!D())return null;const e=new URLSearchParams(window.location.search),i={};for(const t of["utm_source","utm_medium","utm_campaign","utm_term","utm_content"]){const n=e.get(t);n&&(i[t]=n)}return Object.keys(i).length===0?v(c.get(o.UTM)):(c.set(o.UTM,JSON.stringify(i),{expires:w}),i)}getAllData(e){if(!e)return this.transientSession===null&&(this.transientSession={sessionId:crypto.randomUUID(),sessionStart:Date.now()}),{userId:null,...this.transientSession,consent:this.getConsent(),utm:null,isReturningUser:!1};const{sessionId:i,sessionStart:t,isReturningUser:n}=this.initSession();return{userId:e?this.initUser():null,sessionId:i,sessionStart:t,consent:this.getConsent(),utm:this.captureUTM(),isReturningUser:e?n:!1}}}class b{config;queue=[];flushInterval=null;sessionId="";observer=null;intersectionObserver=null;scrollFallbackListener=null;resizeFallbackListener=null;seenInView=new Set;cookieManager=new g;cookieData=null;analyticsConsented=!1;isInitialized=!1;beforeUnloadListener=null;visibilityListener=null;clickListener=null;hoverStartListener=null;hoverEndListener=null;constructor(e){this.config={proxyEndpoint:e.proxyEndpoint??"/api/fp-proxy",batchInterval:e.batchInterval??5e3,debug:e.debug??!1}}init(){!this.isBrowser()||this.isInitialized||(this.isInitialized=!0,this.refreshCookieData(),this.startFlushInterval(),this.attachGlobalListeners(),this.observeDOM())}attachGlobalListeners(){this.clickListener=t=>{const n=t.target instanceof Element?t.target.closest("[data-fp-id]"):null;n&&this.track("click",n.getAttribute("data-fp-id"),{x:t.clientX,y:t.clientY})},document.addEventListener("click",this.clickListener,{passive:!0});let e=0,i=null;this.hoverStartListener=t=>{const n=t.target instanceof Element?t.target.closest("[data-fp-id]"):null;n&&(e=Date.now(),i=n.getAttribute("data-fp-id"))},document.addEventListener("mouseover",this.hoverStartListener,{passive:!0}),this.hoverEndListener=t=>{if(!(t.target instanceof Element?t.target.closest("[data-fp-id]"):null)||!i)return;const a=Date.now()-e;a>500&&this.track("hover",i,{hoverDuration:a}),i=null},document.addEventListener("mouseout",this.hoverEndListener,{passive:!0})}observeDOM(){const e=n=>{this.seenInView.has(n)||(this.seenInView.add(n),this.track("scroll_into_view",n,{scrollDepth:Math.round(window.scrollY/Math.max(1,document.body.scrollHeight)*100)}))};this.intersectionObserver=new IntersectionObserver(n=>{for(const a of n){const s=a.target.dataset.fpId;!s||!a.isIntersecting||e(s)}},{threshold:.1});const i=()=>{document.querySelectorAll("[data-fp-id]").forEach(n=>{this.intersectionObserver?.observe(n)})},t=()=>{const n=window.innerHeight*.1,a=window.innerHeight*.9;document.querySelectorAll("[data-fp-id]").forEach(s=>{const l=s.dataset.fpId;if(!l||this.seenInView.has(l))return;const u=s.getBoundingClientRect();u.bottom>=n&&u.top<=a&&e(l)})};i(),t(),this.scrollFallbackListener=()=>t(),this.resizeFallbackListener=()=>t(),window.addEventListener("scroll",this.scrollFallbackListener,{passive:!0}),window.addEventListener("resize",this.resizeFallbackListener,{passive:!0}),this.observer=new MutationObserver(()=>i()),this.observer.observe(document.body,{childList:!0,subtree:!0})}track(e,i,t){!this.isBrowser()||!this.analyticsConsented||(this.refreshCookieData(),this.queue.push({elementId:i,eventType:e,sessionId:this.cookieData?.sessionId??this.sessionId,visitorId:this.cookieData?.userId??null,isReturningUser:this.cookieData?.isReturningUser??!1,sessionStart:this.cookieData?.sessionStart?new Date(this.cookieData.sessionStart).toISOString():null,utmSource:this.cookieData?.utm?.utm_source??null,utmMedium:this.cookieData?.utm?.utm_medium??null,utmCampaign:this.cookieData?.utm?.utm_campaign??null,utmTerm:this.cookieData?.utm?.utm_term??null,utmContent:this.cookieData?.utm?.utm_content??null,consentAnalytics:this.cookieData?.consent?.analytics??!1,page:window.location.pathname,referrer:document.referrer,device:this.getDeviceType(),browser:this.getBrowser(),os:this.getOS(),metadata:t,timestamp:new Date().toISOString()}))}trackFeedback(e,i,t){this.isBrowser()&&(!i&&!t||(this.refreshCookieData(),this.sendImmediate([{elementId:e??"__page__",eventType:"feedback",sessionId:this.cookieData?.sessionId??this.sessionId,visitorId:this.cookieData?.userId??null,isReturningUser:this.cookieData?.isReturningUser??!1,sessionStart:this.cookieData?.sessionStart?new Date(this.cookieData.sessionStart).toISOString():null,utmSource:this.cookieData?.utm?.utm_source??null,utmMedium:this.cookieData?.utm?.utm_medium??null,utmCampaign:this.cookieData?.utm?.utm_campaign??null,utmTerm:this.cookieData?.utm?.utm_term??null,utmContent:this.cookieData?.utm?.utm_content??null,consentAnalytics:this.cookieData?.consent?.analytics??!1,page:window.location.pathname,device:this.getDeviceType(),metadata:{rating:i,message:t},timestamp:new Date().toISOString()}])))}async flush(){if(this.queue.length===0)return;const e=[...this.queue];this.queue=[],await this.sendImmediate(e)}async sendImmediate(e){try{const i=await fetch(this.config.proxyEndpoint,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({events:e}),keepalive:!0});if(this.config.debug&&i.ok){const t=await i.json().catch(()=>null);t?.ingested!==void 0&&t?.pipeline&&globalThis.console.debug(`[FeedPulse] ${t.ingested} events via ${t.pipeline} pipeline`)}}catch{}}startFlushInterval(){this.flushInterval=setInterval(()=>this.flush(),this.config.batchInterval),this.beforeUnloadListener=()=>this.flush(),this.visibilityListener=()=>{document.visibilityState==="hidden"&&this.flush()},window.addEventListener("beforeunload",this.beforeUnloadListener),document.addEventListener("visibilitychange",this.visibilityListener)}updateCookieData(e){this.cookieData=e,this.sessionId=e.sessionId,this.analyticsConsented=!!e.consent?.analytics}refreshCookieData(){const e=this.cookieManager.getAllData(this.analyticsConsented);this.cookieData=e,this.sessionId=e.sessionId}isBrowser(){return typeof window<"u"&&typeof document<"u"}getDeviceType(){const e=navigator.userAgent;return/Mobi|Android/i.test(e)?"mobile":/Tablet|iPad/i.test(e)?"tablet":"desktop"}getBrowser(){const e=navigator.userAgent;return e.includes("Edg")?"Edge":e.includes("Chrome")?"Chrome":e.includes("Firefox")?"Firefox":e.includes("Safari")?"Safari":"Other"}getOS(){const e=navigator.userAgent;return e.includes("Windows")?"Windows":e.includes("Mac")?"macOS":e.includes("Linux")?"Linux":e.includes("Android")?"Android":/iPhone|iOS/.test(e)?"iOS":"Other"}destroy(){this.flushInterval&&clearInterval(this.flushInterval),this.beforeUnloadListener&&window.removeEventListener("beforeunload",this.beforeUnloadListener),this.visibilityListener&&document.removeEventListener("visibilitychange",this.visibilityListener),this.scrollFallbackListener&&window.removeEventListener("scroll",this.scrollFallbackListener),this.resizeFallbackListener&&window.removeEventListener("resize",this.resizeFallbackListener),this.clickListener&&document.removeEventListener("click",this.clickListener),this.hoverStartListener&&document.removeEventListener("mouseover",this.hoverStartListener),this.hoverEndListener&&document.removeEventListener("mouseout",this.hoverEndListener),this.observer?.disconnect(),this.intersectionObserver?.disconnect(),this.isInitialized=!1,this.flush()}}exports.COOKIE_KEYS=o;exports.CookieManager=g;exports.FeedPulseTracker=b;