@tekibo/feedpulse-sdk 2.1.1 → 2.1.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
@@ -20,7 +20,7 @@ The SDK automatically tracks:
20
20
  - `hover`: when users hover an element for more than 500ms
21
21
  - `scroll_into_view`: when tagged elements enter viewport
22
22
  - `feedback`: explicit rating/message submissions from widget or manual API
23
-
23
+ ## Quick start (Nuxt)
24
24
 
25
25
  Import from `@tekibo/feedpulse-sdk/nuxt` and wrap your app:
26
26
 
@@ -31,16 +31,13 @@ import { FeedPulseProvider, FeedPulseWidget } from "@tekibo/feedpulse-sdk/nuxt";
31
31
 
32
32
  <template>
33
33
  <FeedPulseProvider api-key="fp_live_your_project_key">
34
- <FeedPulseProvider
35
- api-key="fp_live_your_project_key"
36
- endpoint="https://feed-pulse.vercel.app/api/ingest"
37
- >
34
+ <NuxtPage />
38
35
  <FeedPulseWidget id="global-feedback" position="bottom-right" />
39
36
  </FeedPulseProvider>
40
37
  </template>
41
38
  ```
42
39
 
43
-
40
+ The SDK uses `https://feed-pulse.vercel.app/api/ingest` by default, so only `api-key` is required.
44
41
  Tag any interactive element with `data-fp-id`:
45
42
 
46
43
  ```vue
@@ -87,10 +84,7 @@ function SaveButton() {
87
84
 
88
85
  export default function App() {
89
86
  return (
90
- <FeedPulseProvider
91
- apiKey="fp_live_your_project_key"
92
- endpoint="https://feed-pulse.vercel.app/api/ingest"
93
- >
87
+ <FeedPulseProvider apiKey="fp_live_your_project_key">
94
88
  <button data-fp-id="hero-cta">Get Started</button>
95
89
  <SaveButton />
96
90
  <FeedPulseWidget id="global-feedback" position="bottom-right" />
@@ -128,6 +122,13 @@ Nuxt and React widgets support:
128
122
  - `placeholder?: string` prompt text
129
123
  - `position?: "bottom-right" | "bottom-left" | "inline"` default is `"bottom-right"`
130
124
 
125
+ ## Optional endpoint override
126
+
127
+ You can override the default endpoint when needed:
128
+
129
+ - Nuxt: `<FeedPulseProvider api-key="..." endpoint="https://your-domain.com/api/ingest">`
130
+ - React: `<FeedPulseProvider apiKey="..." endpoint="https://your-domain.com/api/ingest">`
131
+
131
132
  ## Integration checklist for your app
132
133
 
133
134
  1. Create a project in FeedPulse dashboard and copy the project API key
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});class n{constructor(e){this.queue=[],this.flushInterval=null,this.observer=null,this.intersectionObserver=null,this.seenInView=new Set,this.config={endpoint:"https://feed-pulse.vercel.app/api/ingest",batchInterval:5e3,...e},this.visitorId=this.getOrCreateVisitorId(),this.sessionId=this.generateSessionId()}init(){this.startFlushInterval(),this.attachGlobalListeners(),this.observeDOM()}attachGlobalListeners(){document.addEventListener("click",i=>{const s=i.target.closest("[data-fp-id]");s&&this.track("click",s.getAttribute("data-fp-id"),{x:i.clientX,y:i.clientY})},{passive:!0});let e=0,t=null;document.addEventListener("mouseover",i=>{const s=i.target.closest("[data-fp-id]");s&&(e=Date.now(),t=s.getAttribute("data-fp-id"))},{passive:!0}),document.addEventListener("mouseout",i=>{if(!i.target.closest("[data-fp-id]")||!t)return;const r=Date.now()-e;r>500&&this.track("hover",t,{hoverDuration:r}),t=null},{passive:!0})}observeDOM(){this.intersectionObserver=new IntersectionObserver(t=>{for(const i of t){const s=i.target.dataset.fpId;!s||!i.isIntersecting||this.seenInView.has(s)||(this.seenInView.add(s),this.track("scroll_into_view",s,{scrollDepth:Math.round(window.scrollY/Math.max(1,document.body.scrollHeight)*100)}))}},{threshold:.5});const e=()=>{document.querySelectorAll("[data-fp-id]").forEach(t=>{this.intersectionObserver?.observe(t)})};e(),this.observer=new MutationObserver(()=>e()),this.observer.observe(document.body,{childList:!0,subtree:!0})}track(e,t,i){this.queue.push({projectApiKey:this.config.apiKey,elementId:t,eventType:e,sessionId:this.sessionId,visitorId:this.visitorId,page:window.location.pathname,referrer:document.referrer,device:this.getDeviceType(),browser:this.getBrowser(),os:this.getOS(),metadata:i,timestamp:new Date().toISOString()})}trackFeedback(e,t,i){!t&&!i||this.sendImmediate([{projectApiKey:this.config.apiKey,elementId:e??"__page__",eventType:"feedback",sessionId:this.sessionId,visitorId:this.visitorId,page:window.location.pathname,device:this.getDeviceType(),metadata:{rating:t,message:i},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{await fetch(this.config.endpoint,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({events:e}),keepalive:!0})}catch{}}startFlushInterval(){this.flushInterval=setInterval(()=>this.flush(),this.config.batchInterval),window.addEventListener("beforeunload",()=>this.flush()),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&this.flush()})}getOrCreateVisitorId(){const e="__fp_vid";let t=localStorage.getItem(e);return t||(t=crypto.randomUUID(),localStorage.setItem(e,t)),t}generateSessionId(){return`${Date.now()}-${Math.random().toString(36).slice(2)}`}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.observer?.disconnect(),this.intersectionObserver?.disconnect(),this.flush()}}exports.FeedPulseTracker=n;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});class n{constructor(e){this.queue=[],this.flushInterval=null,this.observer=null,this.intersectionObserver=null,this.seenInView=new Set;const i="https://feed-pulse.vercel.app/api/ingest";this.config={apiKey:e.apiKey,endpoint:e.endpoint??i,batchInterval:e.batchInterval??5e3},this.visitorId="",this.sessionId=""}init(){this.isBrowser()&&(this.visitorId=this.getOrCreateVisitorId(),this.sessionId=this.generateSessionId(),this.startFlushInterval(),this.attachGlobalListeners(),this.observeDOM())}attachGlobalListeners(){document.addEventListener("click",t=>{const r=t.target.closest("[data-fp-id]");r&&this.track("click",r.getAttribute("data-fp-id"),{x:t.clientX,y:t.clientY})},{passive:!0});let e=0,i=null;document.addEventListener("mouseover",t=>{const r=t.target.closest("[data-fp-id]");r&&(e=Date.now(),i=r.getAttribute("data-fp-id"))},{passive:!0}),document.addEventListener("mouseout",t=>{if(!t.target.closest("[data-fp-id]")||!i)return;const s=Date.now()-e;s>500&&this.track("hover",i,{hoverDuration:s}),i=null},{passive:!0})}observeDOM(){this.intersectionObserver=new IntersectionObserver(i=>{for(const t of i){const r=t.target.dataset.fpId;!r||!t.isIntersecting||this.seenInView.has(r)||(this.seenInView.add(r),this.track("scroll_into_view",r,{scrollDepth:Math.round(window.scrollY/Math.max(1,document.body.scrollHeight)*100)}))}},{threshold:.5});const e=()=>{document.querySelectorAll("[data-fp-id]").forEach(i=>{this.intersectionObserver?.observe(i)})};e(),this.observer=new MutationObserver(()=>e()),this.observer.observe(document.body,{childList:!0,subtree:!0})}track(e,i,t){this.isBrowser()&&this.queue.push({projectApiKey:this.config.apiKey,elementId:i,eventType:e,sessionId:this.sessionId,visitorId:this.visitorId,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.sendImmediate([{projectApiKey:this.config.apiKey,elementId:e??"__page__",eventType:"feedback",sessionId:this.sessionId,visitorId:this.visitorId,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{await fetch(this.config.endpoint,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({events:e}),keepalive:!0})}catch{}}startFlushInterval(){this.flushInterval=setInterval(()=>this.flush(),this.config.batchInterval),window.addEventListener("beforeunload",()=>this.flush()),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&this.flush()})}getOrCreateVisitorId(){const e="__fp_vid",i=this.generateId();try{let t=localStorage.getItem(e);return t||(t=i,localStorage.setItem(e,t)),t}catch{return i}}generateId(){return typeof crypto<"u"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():`${Date.now()}-${Math.random().toString(36).slice(2)}`}isBrowser(){return typeof window<"u"&&typeof document<"u"}generateSessionId(){return`${Date.now()}-${Math.random().toString(36).slice(2)}`}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.observer?.disconnect(),this.intersectionObserver?.disconnect(),this.flush()}}exports.FeedPulseTracker=n;
package/dist/index.mjs CHANGED
@@ -1,53 +1,55 @@
1
1
  class o {
2
2
  constructor(e) {
3
- this.queue = [], this.flushInterval = null, this.observer = null, this.intersectionObserver = null, this.seenInView = /* @__PURE__ */ new Set(), this.config = {
4
- endpoint: "https://feed-pulse.vercel.app/api/ingest",
5
- batchInterval: 5e3,
6
- ...e
7
- }, this.visitorId = this.getOrCreateVisitorId(), this.sessionId = this.generateSessionId();
3
+ this.queue = [], this.flushInterval = null, this.observer = null, this.intersectionObserver = null, this.seenInView = /* @__PURE__ */ new Set();
4
+ const i = "https://feed-pulse.vercel.app/api/ingest";
5
+ this.config = {
6
+ apiKey: e.apiKey,
7
+ endpoint: e.endpoint ?? i,
8
+ batchInterval: e.batchInterval ?? 5e3
9
+ }, this.visitorId = "", this.sessionId = "";
8
10
  }
9
11
  init() {
10
- this.startFlushInterval(), this.attachGlobalListeners(), this.observeDOM();
12
+ this.isBrowser() && (this.visitorId = this.getOrCreateVisitorId(), this.sessionId = this.generateSessionId(), this.startFlushInterval(), this.attachGlobalListeners(), this.observeDOM());
11
13
  }
12
14
  attachGlobalListeners() {
13
- document.addEventListener("click", (i) => {
14
- const s = i.target.closest("[data-fp-id]");
15
- s && this.track("click", s.getAttribute("data-fp-id"), {
16
- x: i.clientX,
17
- y: i.clientY
15
+ document.addEventListener("click", (t) => {
16
+ const r = t.target.closest("[data-fp-id]");
17
+ r && this.track("click", r.getAttribute("data-fp-id"), {
18
+ x: t.clientX,
19
+ y: t.clientY
18
20
  });
19
21
  }, { passive: !0 });
20
- let e = 0, t = null;
21
- document.addEventListener("mouseover", (i) => {
22
- const s = i.target.closest("[data-fp-id]");
23
- s && (e = Date.now(), t = s.getAttribute("data-fp-id"));
24
- }, { passive: !0 }), document.addEventListener("mouseout", (i) => {
25
- if (!i.target.closest("[data-fp-id]") || !t)
22
+ let e = 0, i = null;
23
+ document.addEventListener("mouseover", (t) => {
24
+ const r = t.target.closest("[data-fp-id]");
25
+ r && (e = Date.now(), i = r.getAttribute("data-fp-id"));
26
+ }, { passive: !0 }), document.addEventListener("mouseout", (t) => {
27
+ if (!t.target.closest("[data-fp-id]") || !i)
26
28
  return;
27
- const r = Date.now() - e;
28
- r > 500 && this.track("hover", t, { hoverDuration: r }), t = null;
29
+ const s = Date.now() - e;
30
+ s > 500 && this.track("hover", i, { hoverDuration: s }), i = null;
29
31
  }, { passive: !0 });
30
32
  }
31
33
  observeDOM() {
32
- this.intersectionObserver = new IntersectionObserver((t) => {
33
- for (const i of t) {
34
- const s = i.target.dataset.fpId;
35
- !s || !i.isIntersecting || this.seenInView.has(s) || (this.seenInView.add(s), this.track("scroll_into_view", s, {
34
+ this.intersectionObserver = new IntersectionObserver((i) => {
35
+ for (const t of i) {
36
+ const r = t.target.dataset.fpId;
37
+ !r || !t.isIntersecting || this.seenInView.has(r) || (this.seenInView.add(r), this.track("scroll_into_view", r, {
36
38
  scrollDepth: Math.round(window.scrollY / Math.max(1, document.body.scrollHeight) * 100)
37
39
  }));
38
40
  }
39
41
  }, { threshold: 0.5 });
40
42
  const e = () => {
41
- document.querySelectorAll("[data-fp-id]").forEach((t) => {
42
- this.intersectionObserver?.observe(t);
43
+ document.querySelectorAll("[data-fp-id]").forEach((i) => {
44
+ this.intersectionObserver?.observe(i);
43
45
  });
44
46
  };
45
47
  e(), this.observer = new MutationObserver(() => e()), this.observer.observe(document.body, { childList: !0, subtree: !0 });
46
48
  }
47
- track(e, t, i) {
48
- this.queue.push({
49
+ track(e, i, t) {
50
+ this.isBrowser() && this.queue.push({
49
51
  projectApiKey: this.config.apiKey,
50
- elementId: t,
52
+ elementId: i,
51
53
  eventType: e,
52
54
  sessionId: this.sessionId,
53
55
  visitorId: this.visitorId,
@@ -56,12 +58,12 @@ class o {
56
58
  device: this.getDeviceType(),
57
59
  browser: this.getBrowser(),
58
60
  os: this.getOS(),
59
- metadata: i,
61
+ metadata: t,
60
62
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
61
63
  });
62
64
  }
63
- trackFeedback(e, t, i) {
64
- !t && !i || this.sendImmediate([{
65
+ trackFeedback(e, i, t) {
66
+ this.isBrowser() && (!i && !t || this.sendImmediate([{
65
67
  projectApiKey: this.config.apiKey,
66
68
  elementId: e ?? "__page__",
67
69
  eventType: "feedback",
@@ -69,9 +71,9 @@ class o {
69
71
  visitorId: this.visitorId,
70
72
  page: window.location.pathname,
71
73
  device: this.getDeviceType(),
72
- metadata: { rating: t, message: i },
74
+ metadata: { rating: i, message: t },
73
75
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
74
- }]);
76
+ }]));
75
77
  }
76
78
  async flush() {
77
79
  if (this.queue.length === 0)
@@ -96,9 +98,19 @@ class o {
96
98
  });
97
99
  }
98
100
  getOrCreateVisitorId() {
99
- const e = "__fp_vid";
100
- let t = localStorage.getItem(e);
101
- return t || (t = crypto.randomUUID(), localStorage.setItem(e, t)), t;
101
+ const e = "__fp_vid", i = this.generateId();
102
+ try {
103
+ let t = localStorage.getItem(e);
104
+ return t || (t = i, localStorage.setItem(e, t)), t;
105
+ } catch {
106
+ return i;
107
+ }
108
+ }
109
+ generateId() {
110
+ return typeof crypto < "u" && typeof crypto.randomUUID == "function" ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
111
+ }
112
+ isBrowser() {
113
+ return typeof window < "u" && typeof document < "u";
102
114
  }
103
115
  generateSessionId() {
104
116
  return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekibo/feedpulse-sdk",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
4
4
  "description": "Embeddable analytics and feedback SDK for FeedPulse",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",