@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
@@ -0,0 +1 @@
1
+ "use strict";class d{constructor(t){this.opts=t}opts;score=0;triggered=!1;cooldownUntil=0;reasons=[];clickMap=new Map;lastActivity=Date.now();scrollPositions=[];idleTimer=null;clickHandler=t=>this.onClick(t);scrollHandler=()=>this.onScroll();activityHandler=()=>this.onActivity();errorHandler=()=>this.onError();popstateHandler=()=>this.onBackNavigation();formListeners=[];start(){if(typeof window>"u"||typeof document>"u")return;document.addEventListener("click",this.clickHandler,{passive:!0}),document.addEventListener("scroll",this.scrollHandler,{passive:!0}),document.addEventListener("keydown",this.activityHandler,{passive:!0}),document.addEventListener("mousemove",this.activityHandler,{passive:!0}),window.addEventListener("error",this.errorHandler),window.addEventListener("popstate",this.popstateHandler),document.querySelectorAll("input, textarea, select").forEach(i=>{const s=o=>this.onFormBlur(o);i.addEventListener("blur",s),this.formListeners.push({element:i,handler:s})}),this.idleTimer=setInterval(()=>this.checkIdle(),5e3);const t=`__fp_visit_${window.location.pathname}`,e=Number(sessionStorage.getItem(t)??0)+1;sessionStorage.setItem(t,String(e)),e>=2&&this.addScore(20,"repeated_page")}onClick(t){this.onActivity();const e=t.target;if(!e)return;const i=`${e.tagName}#${e.id}.${e.className}`,s=Date.now(),r=[...this.clickMap.get(i)??[],s].filter(a=>s-a<2e3);this.clickMap.set(i,r),r.length>=3&&this.addScore(40,"rage_click");const n=e.tagName.toLowerCase(),c=["a","button","input","select","textarea"],l=e.onclick||e.getAttribute("onclick");!c.includes(n)&&!l&&!e.closest("a, button")&&this.addScore(20,"dead_click")}onScroll(){this.onActivity();const t=window.scrollY;if(this.scrollPositions=[...this.scrollPositions.slice(-5),t],this.scrollPositions.length>=4){let e=0;for(let i=2;i<this.scrollPositions.length;i++){const s=this.scrollPositions[i-2],o=this.scrollPositions[i-1],r=this.scrollPositions[i];(o-s)*(r-o)<0&&e++}e>=3&&this.addScore(25,"rapid_scroll")}}onError(){this.addScore(35,"js_error")}onBackNavigation(){this.addScore(30,"back_navigation")}onActivity(){this.lastActivity=Date.now()}onFormBlur(t){const e=t.target?.closest("form");if(!e)return;e.querySelector("[data-submitted='true']")||this.addScore(25,"form_abandonment")}checkIdle(){const t=Date.now()-this.lastActivity;t>3e4&&this.addScore(15,"idle"),t>9e4&&this.addScore(20,"long_page_time")}addScore(t,e){Date.now()<this.cooldownUntil||this.triggered||(this.reasons.includes(e)||this.reasons.push(e),this.score+=t,this.score>=60&&(this.triggered=!0,this.cooldownUntil=Date.now()+300*1e3,this.opts.onTrigger(this.score,[...this.reasons])))}reset(){this.score=0,this.triggered=!1,this.reasons=[]}stop(){if(!(typeof window>"u"||typeof document>"u")){document.removeEventListener("click",this.clickHandler),document.removeEventListener("scroll",this.scrollHandler),document.removeEventListener("keydown",this.activityHandler),document.removeEventListener("mousemove",this.activityHandler),window.removeEventListener("error",this.errorHandler),window.removeEventListener("popstate",this.popstateHandler);for(const t of this.formListeners)t.element.removeEventListener("blur",t.handler);this.formListeners=[],this.idleTimer&&clearInterval(this.idleTimer)}}}exports.StruggleDetector=d;
@@ -1,7 +1,22 @@
1
1
  class h {
2
- constructor(e) {
3
- this.opts = e, this.score = 0, this.triggered = !1, this.cooldownUntil = 0, this.reasons = [], this.clickMap = /* @__PURE__ */ new Map(), this.lastActivity = Date.now(), this.scrollPositions = [], this.idleTimer = null, this.clickHandler = (t) => this.onClick(t), this.scrollHandler = () => this.onScroll(), this.activityHandler = () => this.onActivity(), this.errorHandler = () => this.onError(), this.popstateHandler = () => this.onBackNavigation(), this.formListeners = [];
2
+ constructor(t) {
3
+ this.opts = t;
4
4
  }
5
+ opts;
6
+ score = 0;
7
+ triggered = !1;
8
+ cooldownUntil = 0;
9
+ reasons = [];
10
+ clickMap = /* @__PURE__ */ new Map();
11
+ lastActivity = Date.now();
12
+ scrollPositions = [];
13
+ idleTimer = null;
14
+ clickHandler = (t) => this.onClick(t);
15
+ scrollHandler = () => this.onScroll();
16
+ activityHandler = () => this.onActivity();
17
+ errorHandler = () => this.onError();
18
+ popstateHandler = () => this.onBackNavigation();
19
+ formListeners = [];
5
20
  start() {
6
21
  if (typeof window > "u" || typeof document > "u")
7
22
  return;
@@ -9,29 +24,29 @@ class h {
9
24
  const s = (o) => this.onFormBlur(o);
10
25
  i.addEventListener("blur", s), this.formListeners.push({ element: i, handler: s });
11
26
  }), this.idleTimer = setInterval(() => this.checkIdle(), 5e3);
12
- const e = `__fp_visit_${window.location.pathname}`, t = Number(sessionStorage.getItem(e) ?? 0) + 1;
13
- sessionStorage.setItem(e, String(t)), t >= 2 && this.addScore(20, "repeated_page");
27
+ const t = `__fp_visit_${window.location.pathname}`, e = Number(sessionStorage.getItem(t) ?? 0) + 1;
28
+ sessionStorage.setItem(t, String(e)), e >= 2 && this.addScore(20, "repeated_page");
14
29
  }
15
- onClick(e) {
30
+ onClick(t) {
16
31
  this.onActivity();
17
- const t = e.target;
18
- if (!t)
32
+ const e = t.target;
33
+ if (!e)
19
34
  return;
20
- const i = `${t.tagName}#${t.id}.${t.className}`, s = Date.now(), r = [...this.clickMap.get(i) ?? [], s].filter((l) => s - l < 2e3);
35
+ const i = `${e.tagName}#${e.id}.${e.className}`, s = Date.now(), r = [...this.clickMap.get(i) ?? [], s].filter((l) => s - l < 2e3);
21
36
  this.clickMap.set(i, r), r.length >= 3 && this.addScore(40, "rage_click");
22
- const n = t.tagName.toLowerCase(), c = ["a", "button", "input", "select", "textarea"], a = t.onclick || t.getAttribute("onclick");
23
- !c.includes(n) && !a && !t.closest("a, button") && this.addScore(20, "dead_click");
37
+ const n = e.tagName.toLowerCase(), c = ["a", "button", "input", "select", "textarea"], a = e.onclick || e.getAttribute("onclick");
38
+ !c.includes(n) && !a && !e.closest("a, button") && this.addScore(20, "dead_click");
24
39
  }
25
40
  onScroll() {
26
41
  this.onActivity();
27
- const e = window.scrollY;
28
- if (this.scrollPositions = [...this.scrollPositions.slice(-5), e], this.scrollPositions.length >= 4) {
29
- let t = 0;
42
+ const t = window.scrollY;
43
+ if (this.scrollPositions = [...this.scrollPositions.slice(-5), t], this.scrollPositions.length >= 4) {
44
+ let e = 0;
30
45
  for (let i = 2; i < this.scrollPositions.length; i++) {
31
46
  const s = this.scrollPositions[i - 2], o = this.scrollPositions[i - 1], r = this.scrollPositions[i];
32
- (o - s) * (r - o) < 0 && t++;
47
+ (o - s) * (r - o) < 0 && e++;
33
48
  }
34
- t >= 3 && this.addScore(25, "rapid_scroll");
49
+ e >= 3 && this.addScore(25, "rapid_scroll");
35
50
  }
36
51
  }
37
52
  onError() {
@@ -43,18 +58,18 @@ class h {
43
58
  onActivity() {
44
59
  this.lastActivity = Date.now();
45
60
  }
46
- onFormBlur(e) {
47
- const t = e.target?.closest("form");
48
- if (!t)
61
+ onFormBlur(t) {
62
+ const e = t.target?.closest("form");
63
+ if (!e)
49
64
  return;
50
- t.querySelector("[data-submitted='true']") || this.addScore(25, "form_abandonment");
65
+ e.querySelector("[data-submitted='true']") || this.addScore(25, "form_abandonment");
51
66
  }
52
67
  checkIdle() {
53
- const e = Date.now() - this.lastActivity;
54
- e > 3e4 && this.addScore(15, "idle"), e > 9e4 && this.addScore(20, "long_page_time");
68
+ const t = Date.now() - this.lastActivity;
69
+ t > 3e4 && this.addScore(15, "idle"), t > 9e4 && this.addScore(20, "long_page_time");
55
70
  }
56
- addScore(e, t) {
57
- Date.now() < this.cooldownUntil || this.triggered || (this.reasons.includes(t) || this.reasons.push(t), this.score += e, this.score >= 60 && (this.triggered = !0, this.cooldownUntil = Date.now() + 300 * 1e3, this.opts.onTrigger(this.score, [...this.reasons])));
71
+ addScore(t, e) {
72
+ Date.now() < this.cooldownUntil || this.triggered || (this.reasons.includes(e) || this.reasons.push(e), this.score += t, this.score >= 60 && (this.triggered = !0, this.cooldownUntil = Date.now() + 300 * 1e3, this.opts.onTrigger(this.score, [...this.reasons])));
58
73
  }
59
74
  reset() {
60
75
  this.score = 0, this.triggered = !1, this.reasons = [];
@@ -62,8 +77,8 @@ class h {
62
77
  stop() {
63
78
  if (!(typeof window > "u" || typeof document > "u")) {
64
79
  document.removeEventListener("click", this.clickHandler), document.removeEventListener("scroll", this.scrollHandler), document.removeEventListener("keydown", this.activityHandler), document.removeEventListener("mousemove", this.activityHandler), window.removeEventListener("error", this.errorHandler), window.removeEventListener("popstate", this.popstateHandler);
65
- for (const e of this.formListeners)
66
- e.element.removeEventListener("blur", e.handler);
80
+ for (const t of this.formListeners)
81
+ t.element.removeEventListener("blur", t.handler);
67
82
  this.formListeners = [], this.idleTimer && clearInterval(this.idleTimer);
68
83
  }
69
84
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekibo/feedpulse-sdk",
3
- "version": "0.7.3",
3
+ "version": "0.8.0",
4
4
  "description": "Embeddable analytics and feedback SDK for FeedPulse",
5
5
  "exports": {
6
6
  ".": {
@@ -14,10 +14,6 @@
14
14
  "./react": {
15
15
  "import": "./dist/react.mjs",
16
16
  "require": "./dist/react.js"
17
- },
18
- "./server": {
19
- "import": "./dist/server.mjs",
20
- "require": "./dist/server.js"
21
17
  }
22
18
  },
23
19
  "main": "./dist/index.js",
@@ -25,12 +21,8 @@
25
21
  "types": "./dist/index.d.ts",
26
22
  "files": [
27
23
  "dist",
28
- "bin",
29
24
  "templates"
30
25
  ],
31
- "bin": {
32
- "feedpulse-bot-build": "./bin/feedpulse-bot-build.mjs"
33
- },
34
26
  "scripts": {
35
27
  "build": "vite build",
36
28
  "dev": "vite build --watch"
@@ -48,16 +40,17 @@
48
40
  }
49
41
  },
50
42
  "dependencies": {
51
- "js-cookie": "^3.0.5",
52
- "zod": "^4.3.5"
43
+ "js-cookie": "^3.0.5"
53
44
  },
54
45
  "devDependencies": {
55
46
  "@types/js-cookie": "^3.0.6",
47
+ "@types/react": "^19.3.0",
56
48
  "@vitejs/plugin-react": "^4.7.0",
57
49
  "@vitejs/plugin-vue": "^6.0.1",
58
50
  "react": "^19.1.1",
59
51
  "typescript": "^5.9.3",
60
52
  "vite": "^7.1.12",
53
+ "vite-plugin-dts": "^5.1.0",
61
54
  "vue": "^3.5.26"
62
55
  }
63
56
  }
@@ -1,36 +1,36 @@
1
- import type { NextRequest } from "next/server";
2
-
3
- import { NextResponse } from "next/server";
4
-
5
- export async function POST(request: NextRequest) {
6
- const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env ?? {};
7
- const apiKey = env.FEEDPULSE_API_KEY;
8
- const workerUrl = env.FEEDPULSE_WORKER_URL || "https://feedpulse-workers.tekibohelp.workers.dev/ingest";
9
-
10
- if (!apiKey) {
11
- return NextResponse.json(
12
- { error: "FEEDPULSE_API_KEY is not configured" },
13
- { status: 500 },
14
- );
15
- }
16
-
17
- const body = await request.json().catch(() => null);
18
- const events = Array.isArray(body?.events) ? body.events : [];
19
- if (events.length === 0)
20
- return NextResponse.json({ error: "No events" }, { status: 400 });
21
-
22
- const enriched = events.map((item: unknown) => {
23
- if (!item || typeof item !== "object")
24
- return { projectApiKey: apiKey };
25
- return { ...item, projectApiKey: apiKey };
26
- });
27
-
28
- const response = await fetch(workerUrl, {
29
- method: "POST",
30
- headers: { "content-type": "application/json" },
31
- body: JSON.stringify({ events: enriched }),
32
- });
33
-
34
- const payload = await response.json().catch(() => ({ ok: false, error: "Invalid worker response" }));
35
- return NextResponse.json(payload, { status: response.status });
36
- }
1
+ import type { NextRequest } from "next/server";
2
+
3
+ import { NextResponse } from "next/server";
4
+
5
+ export async function POST(request: NextRequest) {
6
+ const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env ?? {};
7
+ const apiKey = env.FEEDPULSE_API_KEY;
8
+ const workerUrl = env.FEEDPULSE_WORKER_URL || "https://feedpulse-workers.tekibohelp.workers.dev/ingest";
9
+
10
+ if (!apiKey) {
11
+ return NextResponse.json(
12
+ { error: "FEEDPULSE_API_KEY is not configured" },
13
+ { status: 500 },
14
+ );
15
+ }
16
+
17
+ const body = await request.json().catch(() => null);
18
+ const events = Array.isArray(body?.events) ? body.events : [];
19
+ if (events.length === 0)
20
+ return NextResponse.json({ error: "No events" }, { status: 400 });
21
+
22
+ const enriched = events.map((item: unknown) => {
23
+ if (!item || typeof item !== "object")
24
+ return { projectApiKey: apiKey };
25
+ return { ...item, projectApiKey: apiKey };
26
+ });
27
+
28
+ const response = await fetch(workerUrl, {
29
+ method: "POST",
30
+ headers: { "content-type": "application/json" },
31
+ body: JSON.stringify({ events: enriched }),
32
+ });
33
+
34
+ const payload = await response.json().catch(() => ({ ok: false, error: "Invalid worker response" }));
35
+ return NextResponse.json(payload, { status: response.status });
36
+ }
@@ -1,93 +1,60 @@
1
- export const chatProxyRoute = `import { existsSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
3
-
4
- import { zodToJsonSchema } from "zod-to-json-schema";
5
- import type { FeedPulseTool } from "@tekibo/feedpulse-sdk";
6
-
7
- export default defineEventHandler(async (event) => {
8
- const config = useRuntimeConfig(event);
9
- const apiKey = config.feedpulseApiKey;
10
- const feedpulseBaseUrl = config.feedpulseApiBaseUrl || "http://localhost:3000";
11
- if (!apiKey)
12
- throw createError({ statusCode: 500, statusMessage: "FEEDPULSE_API_KEY is not configured" });
13
-
14
- const body = await readBody<Record<string, unknown>>(event);
15
- const page = typeof body.page === "string" ? body.page : "/";
16
- const mapKey = page.replace(/\\//g, "_").replace(/^_/, "") || "index";
17
- const mapPath = join(process.cwd(), "feedpulse", "page-maps", \`\${mapKey}.json\`);
18
- const pageMap = existsSync(mapPath) ? JSON.parse(readFileSync(mapPath, "utf8")) : null;
19
- let toolsPayload: Array<Record<string, unknown>> = [];
20
- try {
21
- const module = await import("~~/feedpulse/tools") as { feedpulseTools?: FeedPulseTool[] };
22
- const tools = module.feedpulseTools || [];
23
- toolsPayload = tools.map(item => ({
24
- name: item.name,
25
- description: item.description,
26
- inputSchema: zodToJsonSchema(item.inputSchema, item.name),
27
- endpoint: item.endpoint,
28
- method: item.method ?? "POST",
29
- }));
30
- }
31
- catch {}
32
- const response = await fetch(\`\${feedpulseBaseUrl}/api/bot/chat\`, {
33
- method: "POST",
34
- headers: { "content-type": "application/json" },
35
- body: JSON.stringify({ ...body, projectApiKey: apiKey, developerTools: toolsPayload, pageMap }),
36
- });
37
-
38
- return new Response(response.body, {
39
- status: response.status,
40
- headers: {
41
- "content-type": response.headers.get("content-type") || "text/plain; charset=utf-8",
42
- },
43
- });
44
- });`;
45
-
46
- export const sessionProxyRoute = `export default defineEventHandler(async (event) => {
47
- const config = useRuntimeConfig(event);
48
- const apiKey = config.feedpulseApiKey;
49
- const feedpulseBaseUrl = config.feedpulseApiBaseUrl || "http://localhost:3000";
50
- if (!apiKey)
51
- throw createError({ statusCode: 500, statusMessage: "FEEDPULSE_API_KEY is not configured" });
52
-
53
- const body = await readBody<Record<string, unknown>>(event);
54
- const response = await fetch(\`\${feedpulseBaseUrl}/api/bot/session/start\`, {
55
- method: "POST",
56
- headers: { "content-type": "application/json" },
57
- body: JSON.stringify({ ...body, projectApiKey: apiKey }),
58
- });
59
-
60
- const payload = await response.json().catch(() => ({ error: "Invalid response" }));
61
- if (!response.ok)
62
- throw createError({ statusCode: response.status, statusMessage: payload?.error || "Session proxy failed", data: payload });
63
- return payload;
64
- });`;
65
-
66
- export const sessionEndProxyRoute = `export default defineEventHandler(async (event) => {
67
- const config = useRuntimeConfig(event);
68
- const feedpulseBaseUrl = config.feedpulseApiBaseUrl || "http://localhost:3000";
69
- const body = await readBody<Record<string, unknown>>(event);
70
- const response = await fetch(\`\${feedpulseBaseUrl}/api/bot/session/end\`, {
71
- method: "POST",
72
- headers: { "content-type": "application/json" },
73
- body: JSON.stringify(body),
74
- });
75
- const payload = await response.json().catch(() => ({ error: "Invalid response" }));
76
- if (!response.ok)
77
- throw createError({ statusCode: response.status, statusMessage: payload?.error || "Session end proxy failed", data: payload });
78
- return payload;
79
- });`;
80
-
81
- export const configIdRoute = `import { encryptConfigId } from "@tekibo/feedpulse-sdk/server";
82
-
83
- export default defineEventHandler(async (event) => {
84
- const query = getQuery(event);
85
- const key = String(query.key || "");
86
- if (!key)
87
- throw createError({ statusCode: 400, statusMessage: "key is required" });
88
- const config = useRuntimeConfig(event);
89
- const secret = config.feedpulseEncryptKey;
90
- if (!secret || secret.length < 16)
91
- throw createError({ statusCode: 500, statusMessage: "FEEDPULSE_ENCRYPT_KEY must be at least 16 characters." });
92
- return { encryptedId: encryptConfigId(key, secret) };
93
- });`;
1
+ export const chatProxyRoute = `export default defineEventHandler(async (event) => {
2
+ const config = useRuntimeConfig(event);
3
+ const apiKey = config.feedpulseApiKey;
4
+ const feedpulseBaseUrl = config.feedpulseApiBaseUrl || "http://localhost:3000";
5
+ if (!apiKey)
6
+ throw createError({ statusCode: 500, statusMessage: "FEEDPULSE_API_KEY is not configured" });
7
+
8
+ const body = await readBody<Record<string, unknown>>(event);
9
+ const response = await fetch(\`\${feedpulseBaseUrl}/api/bot/chat\`, {
10
+ method: "POST",
11
+ headers: { "content-type": "application/json" },
12
+ body: JSON.stringify({ ...body, projectApiKey: apiKey }),
13
+ });
14
+
15
+ return new Response(response.body, {
16
+ status: response.status,
17
+ headers: {
18
+ "content-type": response.headers.get("content-type") || "text/plain; charset=utf-8",
19
+ },
20
+ });
21
+ });`;
22
+
23
+ export const sessionProxyRoute = `export default defineEventHandler(async (event) => {
24
+ const config = useRuntimeConfig(event);
25
+ const apiKey = config.feedpulseApiKey;
26
+ const feedpulseBaseUrl = config.feedpulseApiBaseUrl || "http://localhost:3000";
27
+ if (!apiKey)
28
+ throw createError({ statusCode: 500, statusMessage: "FEEDPULSE_API_KEY is not configured" });
29
+
30
+ const body = await readBody<Record<string, unknown>>(event);
31
+ const response = await fetch(\`\${feedpulseBaseUrl}/api/bot/session/start\`, {
32
+ method: "POST",
33
+ headers: { "content-type": "application/json" },
34
+ body: JSON.stringify({ ...body, projectApiKey: apiKey }),
35
+ });
36
+
37
+ const payload = await response.json().catch(() => ({ error: "Invalid response" }));
38
+ if (!response.ok)
39
+ throw createError({ statusCode: response.status, statusMessage: payload?.error || "Session proxy failed", data: payload });
40
+ return payload;
41
+ });`;
42
+
43
+ export const sessionEndProxyRoute = `export default defineEventHandler(async (event) => {
44
+ const config = useRuntimeConfig(event);
45
+ const apiKey = config.feedpulseApiKey;
46
+ const feedpulseBaseUrl = config.feedpulseApiBaseUrl || "http://localhost:3000";
47
+ if (!apiKey)
48
+ throw createError({ statusCode: 500, statusMessage: "FEEDPULSE_API_KEY is not configured" });
49
+
50
+ const body = await readBody<Record<string, unknown>>(event);
51
+ const response = await fetch(\`\${feedpulseBaseUrl}/api/bot/session/end\`, {
52
+ method: "POST",
53
+ headers: { "content-type": "application/json" },
54
+ body: JSON.stringify({ ...body, projectApiKey: apiKey }),
55
+ });
56
+ const payload = await response.json().catch(() => ({ error: "Invalid response" }));
57
+ if (!response.ok)
58
+ throw createError({ statusCode: response.status, statusMessage: payload?.error || "Session end proxy failed", data: payload });
59
+ return payload;
60
+ });`;
@@ -1,44 +1,44 @@
1
- export default defineEventHandler(async (event) => {
2
- const config = useRuntimeConfig(event);
3
- const apiKey = config.feedpulseApiKey;
4
- const workerUrl = config.feedpulseWorkerUrl || "https://feedpulse-workers.tekibohelp.workers.dev/ingest";
5
-
6
- if (!apiKey) {
7
- throw createError({
8
- statusCode: 500,
9
- statusMessage: "FEEDPULSE_API_KEY is not configured",
10
- });
11
- }
12
-
13
- const body = await readBody<{ events?: unknown }>(event);
14
- const events = Array.isArray(body?.events) ? body.events : [];
15
- if (events.length === 0) {
16
- throw createError({
17
- statusCode: 400,
18
- statusMessage: "No events",
19
- });
20
- }
21
-
22
- const enriched = events.map((item) => {
23
- if (!item || typeof item !== "object")
24
- return { projectApiKey: apiKey };
25
- return { ...item, projectApiKey: apiKey };
26
- });
27
-
28
- const response = await fetch(workerUrl, {
29
- method: "POST",
30
- headers: { "content-type": "application/json" },
31
- body: JSON.stringify({ events: enriched }),
32
- });
33
-
34
- const payload = await response.json().catch(() => ({ ok: false, error: "Invalid worker response" }));
35
- if (!response.ok) {
36
- throw createError({
37
- statusCode: response.status,
38
- statusMessage: typeof payload?.error === "string" ? payload.error : "FeedPulse worker request failed",
39
- data: payload,
40
- });
41
- }
42
-
43
- return payload;
44
- });
1
+ export default defineEventHandler(async (event) => {
2
+ const config = useRuntimeConfig(event);
3
+ const apiKey = config.feedpulseApiKey;
4
+ const workerUrl = config.feedpulseWorkerUrl || "https://feedpulse-workers.tekibohelp.workers.dev/ingest";
5
+
6
+ if (!apiKey) {
7
+ throw createError({
8
+ statusCode: 500,
9
+ statusMessage: "FEEDPULSE_API_KEY is not configured",
10
+ });
11
+ }
12
+
13
+ const body = await readBody<{ events?: unknown }>(event);
14
+ const events = Array.isArray(body?.events) ? body.events : [];
15
+ if (events.length === 0) {
16
+ throw createError({
17
+ statusCode: 400,
18
+ statusMessage: "No events",
19
+ });
20
+ }
21
+
22
+ const enriched = events.map((item) => {
23
+ if (!item || typeof item !== "object")
24
+ return { projectApiKey: apiKey };
25
+ return { ...item, projectApiKey: apiKey };
26
+ });
27
+
28
+ const response = await fetch(workerUrl, {
29
+ method: "POST",
30
+ headers: { "content-type": "application/json" },
31
+ body: JSON.stringify({ events: enriched }),
32
+ });
33
+
34
+ const payload = await response.json().catch(() => ({ ok: false, error: "Invalid worker response" }));
35
+ if (!response.ok) {
36
+ throw createError({
37
+ statusCode: response.status,
38
+ statusMessage: typeof payload?.error === "string" ? payload.error : "FeedPulse worker request failed",
39
+ data: payload,
40
+ });
41
+ }
42
+
43
+ return payload;
44
+ });
@@ -1,121 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { existsSync } from "node:fs";
4
- import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
5
- import { join, relative } from "node:path";
6
-
7
- const CWD = process.cwd();
8
- const PAGES_DIR = join(CWD, "pages");
9
- const OUTPUT_DIR = join(CWD, "feedpulse", "page-maps");
10
-
11
- async function findBotPages(dir) {
12
- const results = [];
13
- for (const entry of await readdir(dir, { withFileTypes: true })) {
14
- const filePath = join(dir, entry.name);
15
- if (entry.isDirectory()) {
16
- results.push(...await findBotPages(filePath));
17
- }
18
- else if (entry.isFile() && /\.(vue|tsx)$/.test(entry.name)) {
19
- const content = await readFile(filePath, "utf8");
20
- if (content.includes("FeedPulseBot")) {
21
- const route = `/${relative(PAGES_DIR, filePath)
22
- .replace(/\.(vue|tsx)$/, "")
23
- .replace(/\/index$/, "")
24
- .replace(/\\/g, "/")}`;
25
- results.push({ path: filePath, content, route });
26
- }
27
- }
28
- }
29
- return results;
30
- }
31
-
32
- function extractBotProps(content) {
33
- const botMatch = content.match(/<FeedPulseBot([^/]*?)(?:\/?>|\n\s*>)/s);
34
- if (!botMatch)
35
- return {};
36
- const propsStr = botMatch[1];
37
- const get = (name) => {
38
- const match = propsStr.match(new RegExp(`:?${name}="([^"]*)"`, "i"));
39
- if (!match)
40
- return undefined;
41
- try {
42
- return JSON.parse(match[1].replace(/'/g, "\""));
43
- }
44
- catch {
45
- return match[1];
46
- }
47
- };
48
- return {
49
- pageContext: get("page-context") ?? get("pageContext"),
50
- systemPrompt: get("system-prompt") ?? get("systemPrompt"),
51
- position: get("position"),
52
- };
53
- }
54
-
55
- function extractInteractiveElements(content) {
56
- const elements = [];
57
- const patterns = [
58
- [/data-fp-id="([^"]+)"/g, "tracked"],
59
- [/<button[^>]*>([^<]*)<\/button>/gi, "button"],
60
- [/<a\s[^>]*href="([^"]*)"[^>]*>([^<]*)<\/a>/gi, "link"],
61
- [/<input[^>]*(?:placeholder|id)="([^"]*)"/gi, "input"],
62
- ];
63
- for (const [pattern, type] of patterns) {
64
- let match;
65
- while ((match = pattern.exec(content)) !== null)
66
- elements.push({ type, value: match[1] ?? match[0] });
67
- }
68
- return elements;
69
- }
70
-
71
- function buildPageMap(route, props, elements, rawContent) {
72
- return {
73
- route,
74
- generatedAt: new Date().toISOString(),
75
- botConfig: props,
76
- interactiveElements: elements,
77
- templateSummary: rawContent
78
- .replace(/<script[\s\S]*?<\/script>/gi, "")
79
- .replace(/<style[\s\S]*?<\/style>/gi, "")
80
- .replace(/<!--[\s\S]*?-->/g, "")
81
- .replace(/\s+/g, " ")
82
- .trim()
83
- .slice(0, 3000),
84
- injectionProtection: { enabled: true },
85
- };
86
- }
87
-
88
- async function run() {
89
- if (!existsSync(PAGES_DIR)) {
90
- console.error("No pages/ dir found.");
91
- process.exit(1);
92
- }
93
-
94
- await mkdir(OUTPUT_DIR, { recursive: true });
95
- const pages = await findBotPages(PAGES_DIR);
96
- console.log(`FeedPulse Bot Build — found ${pages.length} page(s) with FeedPulseBot`);
97
-
98
- for (const page of pages) {
99
- const map = buildPageMap(
100
- page.route,
101
- extractBotProps(page.content),
102
- extractInteractiveElements(page.content),
103
- page.content,
104
- );
105
- const filename = page.route.replace(/\//g, "_").replace(/^_/, "") || "index";
106
- await writeFile(join(OUTPUT_DIR, `${filename}.json`), JSON.stringify(map, null, 2));
107
- console.log(`${page.route} -> feedpulse/page-maps/${filename}.json`);
108
- }
109
-
110
- await writeFile(join(OUTPUT_DIR, "_index.json"), JSON.stringify({
111
- generatedAt: new Date().toISOString(),
112
- pages: pages.map(page => ({ route: page.route })),
113
- }, null, 2));
114
-
115
- console.log("Done. Page maps in feedpulse/page-maps/");
116
- }
117
-
118
- run().catch((error) => {
119
- console.error(error);
120
- process.exit(1);
121
- });
package/dist/server.js DELETED
@@ -1 +0,0 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const c=require("node:crypto");function i(t){if(t.length<16)throw new Error("FEEDPULSE_ENCRYPT_KEY must be at least 16 characters.");return c.createHash("sha256").update(t).digest()}function f(t,a){const e=i(a),n=c.randomBytes(12),r=c.createCipheriv("aes-256-gcm",e,n),o=Buffer.concat([r.update(t,"utf8"),r.final()]),s=r.getAuthTag();return Buffer.concat([n,s,o]).toString("base64url")}function d(t,a){const e=Buffer.from(t,"base64url"),n=e.subarray(0,12),r=e.subarray(12,28),o=e.subarray(28),s=i(a),u=c.createDecipheriv("aes-256-gcm",s,n);return u.setAuthTag(r),u.update(o,void 0,"utf8")+u.final("utf8")}exports.decryptConfigId=d;exports.encryptConfigId=f;
package/dist/server.mjs DELETED
@@ -1,18 +0,0 @@
1
- import { createDecipheriv as i, randomBytes as f, createCipheriv as d, createHash as g } from "node:crypto";
2
- function u(t) {
3
- if (t.length < 16)
4
- throw new Error("FEEDPULSE_ENCRYPT_KEY must be at least 16 characters.");
5
- return g("sha256").update(t).digest();
6
- }
7
- function h(t, n) {
8
- const e = u(n), a = f(12), r = d("aes-256-gcm", e, a), c = Buffer.concat([r.update(t, "utf8"), r.final()]), o = r.getAuthTag();
9
- return Buffer.concat([a, o, c]).toString("base64url");
10
- }
11
- function y(t, n) {
12
- const e = Buffer.from(t, "base64url"), a = e.subarray(0, 12), r = e.subarray(12, 28), c = e.subarray(28), o = u(n), s = i("aes-256-gcm", o, a);
13
- return s.setAuthTag(r), s.update(c, void 0, "utf8") + s.final("utf8");
14
- }
15
- export {
16
- y as decryptConfigId,
17
- h as encryptConfigId
18
- };
@@ -1 +0,0 @@
1
- "use strict";class d{constructor(e){this.opts=e,this.score=0,this.triggered=!1,this.cooldownUntil=0,this.reasons=[],this.clickMap=new Map,this.lastActivity=Date.now(),this.scrollPositions=[],this.idleTimer=null,this.clickHandler=t=>this.onClick(t),this.scrollHandler=()=>this.onScroll(),this.activityHandler=()=>this.onActivity(),this.errorHandler=()=>this.onError(),this.popstateHandler=()=>this.onBackNavigation(),this.formListeners=[]}start(){if(typeof window>"u"||typeof document>"u")return;document.addEventListener("click",this.clickHandler,{passive:!0}),document.addEventListener("scroll",this.scrollHandler,{passive:!0}),document.addEventListener("keydown",this.activityHandler,{passive:!0}),document.addEventListener("mousemove",this.activityHandler,{passive:!0}),window.addEventListener("error",this.errorHandler),window.addEventListener("popstate",this.popstateHandler),document.querySelectorAll("input, textarea, select").forEach(i=>{const s=o=>this.onFormBlur(o);i.addEventListener("blur",s),this.formListeners.push({element:i,handler:s})}),this.idleTimer=setInterval(()=>this.checkIdle(),5e3);const e=`__fp_visit_${window.location.pathname}`,t=Number(sessionStorage.getItem(e)??0)+1;sessionStorage.setItem(e,String(t)),t>=2&&this.addScore(20,"repeated_page")}onClick(e){this.onActivity();const t=e.target;if(!t)return;const i=`${t.tagName}#${t.id}.${t.className}`,s=Date.now(),r=[...this.clickMap.get(i)??[],s].filter(a=>s-a<2e3);this.clickMap.set(i,r),r.length>=3&&this.addScore(40,"rage_click");const n=t.tagName.toLowerCase(),c=["a","button","input","select","textarea"],l=t.onclick||t.getAttribute("onclick");!c.includes(n)&&!l&&!t.closest("a, button")&&this.addScore(20,"dead_click")}onScroll(){this.onActivity();const e=window.scrollY;if(this.scrollPositions=[...this.scrollPositions.slice(-5),e],this.scrollPositions.length>=4){let t=0;for(let i=2;i<this.scrollPositions.length;i++){const s=this.scrollPositions[i-2],o=this.scrollPositions[i-1],r=this.scrollPositions[i];(o-s)*(r-o)<0&&t++}t>=3&&this.addScore(25,"rapid_scroll")}}onError(){this.addScore(35,"js_error")}onBackNavigation(){this.addScore(30,"back_navigation")}onActivity(){this.lastActivity=Date.now()}onFormBlur(e){const t=e.target?.closest("form");if(!t)return;t.querySelector("[data-submitted='true']")||this.addScore(25,"form_abandonment")}checkIdle(){const e=Date.now()-this.lastActivity;e>3e4&&this.addScore(15,"idle"),e>9e4&&this.addScore(20,"long_page_time")}addScore(e,t){Date.now()<this.cooldownUntil||this.triggered||(this.reasons.includes(t)||this.reasons.push(t),this.score+=e,this.score>=60&&(this.triggered=!0,this.cooldownUntil=Date.now()+300*1e3,this.opts.onTrigger(this.score,[...this.reasons])))}reset(){this.score=0,this.triggered=!1,this.reasons=[]}stop(){if(!(typeof window>"u"||typeof document>"u")){document.removeEventListener("click",this.clickHandler),document.removeEventListener("scroll",this.scrollHandler),document.removeEventListener("keydown",this.activityHandler),document.removeEventListener("mousemove",this.activityHandler),window.removeEventListener("error",this.errorHandler),window.removeEventListener("popstate",this.popstateHandler);for(const e of this.formListeners)e.element.removeEventListener("blur",e.handler);this.formListeners=[],this.idleTimer&&clearInterval(this.idleTimer)}}}exports.StruggleDetector=d;