@interfere/react 0.1.0-alpha.2 → 0.1.0-alpha.26

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 (206) hide show
  1. package/README.md +90 -0
  2. package/dist/error-boundary.d.mts +27 -0
  3. package/dist/error-boundary.d.mts.map +1 -0
  4. package/dist/error-boundary.mjs +42 -0
  5. package/dist/error-boundary.mjs.map +1 -0
  6. package/dist/internal/client.d.mts +13 -0
  7. package/dist/internal/client.d.mts.map +1 -0
  8. package/dist/internal/client.mjs +80 -0
  9. package/dist/internal/client.mjs.map +1 -0
  10. package/dist/internal/config.d.mts +9 -0
  11. package/dist/internal/config.d.mts.map +1 -0
  12. package/dist/internal/config.mjs +27 -0
  13. package/dist/internal/config.mjs.map +1 -0
  14. package/dist/internal/context.d.mts +6 -0
  15. package/dist/internal/context.d.mts.map +1 -0
  16. package/dist/internal/context.mjs +32 -0
  17. package/dist/internal/context.mjs.map +1 -0
  18. package/dist/internal/envelope.d.mts +14 -0
  19. package/dist/internal/envelope.d.mts.map +1 -0
  20. package/dist/internal/envelope.mjs +20 -0
  21. package/dist/internal/envelope.mjs.map +1 -0
  22. package/dist/internal/errors.d.mts +4 -0
  23. package/dist/internal/errors.d.mts.map +1 -0
  24. package/dist/internal/errors.mjs +4 -0
  25. package/dist/internal/errors.mjs.map +1 -0
  26. package/dist/internal/sw.d.mts +4 -0
  27. package/dist/internal/sw.d.mts.map +1 -0
  28. package/dist/internal/sw.mjs +10 -0
  29. package/dist/internal/sw.mjs.map +1 -0
  30. package/dist/plugins/errors.d.mts +6 -0
  31. package/dist/plugins/errors.d.mts.map +1 -0
  32. package/dist/plugins/errors.mjs +59 -0
  33. package/dist/plugins/errors.mjs.map +1 -0
  34. package/dist/plugins/lib/loader.d.mts +9 -0
  35. package/dist/plugins/lib/loader.d.mts.map +1 -0
  36. package/dist/plugins/lib/loader.mjs +47 -0
  37. package/dist/plugins/lib/loader.mjs.map +1 -0
  38. package/dist/plugins/lib/types.d.mts +14 -0
  39. package/dist/plugins/lib/types.d.mts.map +1 -0
  40. package/dist/plugins/lib/types.mjs +1 -0
  41. package/dist/plugins/pages.d.mts +6 -0
  42. package/dist/plugins/pages.d.mts.map +1 -0
  43. package/dist/plugins/pages.mjs +102 -0
  44. package/dist/plugins/pages.mjs.map +1 -0
  45. package/dist/plugins/rage-clicks.d.mts +6 -0
  46. package/dist/plugins/rage-clicks.d.mts.map +1 -0
  47. package/dist/plugins/rage-clicks.mjs +53 -0
  48. package/dist/plugins/rage-clicks.mjs.map +1 -0
  49. package/dist/plugins/replay.d.mts +6 -0
  50. package/dist/plugins/replay.d.mts.map +1 -0
  51. package/dist/plugins/replay.mjs +62 -0
  52. package/dist/plugins/replay.mjs.map +1 -0
  53. package/dist/provider.d.mts +17 -11
  54. package/dist/provider.d.mts.map +1 -1
  55. package/dist/provider.mjs +18 -17
  56. package/dist/provider.mjs.map +1 -1
  57. package/dist/tracking/api.d.mts +22 -0
  58. package/dist/tracking/api.d.mts.map +1 -0
  59. package/dist/tracking/api.mjs +88 -0
  60. package/dist/tracking/api.mjs.map +1 -0
  61. package/dist/tracking/session.d.mts +19 -0
  62. package/dist/tracking/session.d.mts.map +1 -0
  63. package/dist/tracking/session.mjs +92 -0
  64. package/dist/tracking/session.mjs.map +1 -0
  65. package/dist/tracking/visitor.d.mts +6 -0
  66. package/dist/tracking/visitor.d.mts.map +1 -0
  67. package/dist/tracking/visitor.mjs +35 -0
  68. package/dist/tracking/visitor.mjs.map +1 -0
  69. package/dist/transport/http.d.mts +15 -0
  70. package/dist/transport/http.d.mts.map +1 -0
  71. package/dist/transport/http.mjs +56 -0
  72. package/dist/transport/http.mjs.map +1 -0
  73. package/dist/transport/queue.d.mts +25 -0
  74. package/dist/transport/queue.d.mts.map +1 -0
  75. package/dist/transport/queue.mjs +60 -0
  76. package/dist/transport/queue.mjs.map +1 -0
  77. package/dist/util/log.d.mts +13 -0
  78. package/dist/util/log.d.mts.map +1 -0
  79. package/dist/util/log.mjs +37 -0
  80. package/dist/util/log.mjs.map +1 -0
  81. package/package.json +43 -66
  82. package/dist/client.d.mts +0 -15
  83. package/dist/client.d.mts.map +0 -1
  84. package/dist/client.mjs +0 -75
  85. package/dist/client.mjs.map +0 -1
  86. package/dist/core/events/event-registry.d.mts +0 -23
  87. package/dist/core/events/event-registry.d.mts.map +0 -1
  88. package/dist/core/events/event-registry.mjs +0 -32
  89. package/dist/core/events/event-registry.mjs.map +0 -1
  90. package/dist/core/events/plugin-event-types.d.mts +0 -92
  91. package/dist/core/events/plugin-event-types.d.mts.map +0 -1
  92. package/dist/core/events/plugin-event-types.mjs +0 -25
  93. package/dist/core/events/plugin-event-types.mjs.map +0 -1
  94. package/dist/core/plugins/dom-utils.d.mts +0 -9
  95. package/dist/core/plugins/dom-utils.d.mts.map +0 -1
  96. package/dist/core/plugins/dom-utils.mjs +0 -25
  97. package/dist/core/plugins/dom-utils.mjs.map +0 -1
  98. package/dist/core/plugins/impl/ai-summary.d.mts +0 -6
  99. package/dist/core/plugins/impl/ai-summary.d.mts.map +0 -1
  100. package/dist/core/plugins/impl/ai-summary.mjs +0 -122
  101. package/dist/core/plugins/impl/ai-summary.mjs.map +0 -1
  102. package/dist/core/plugins/impl/errors.d.mts +0 -9
  103. package/dist/core/plugins/impl/errors.d.mts.map +0 -1
  104. package/dist/core/plugins/impl/errors.mjs +0 -153
  105. package/dist/core/plugins/impl/errors.mjs.map +0 -1
  106. package/dist/core/plugins/impl/page-events.d.mts +0 -15
  107. package/dist/core/plugins/impl/page-events.d.mts.map +0 -1
  108. package/dist/core/plugins/impl/page-events.mjs +0 -131
  109. package/dist/core/plugins/impl/page-events.mjs.map +0 -1
  110. package/dist/core/plugins/impl/rage-click.d.mts +0 -6
  111. package/dist/core/plugins/impl/rage-click.d.mts.map +0 -1
  112. package/dist/core/plugins/impl/rage-click.mjs +0 -53
  113. package/dist/core/plugins/impl/rage-click.mjs.map +0 -1
  114. package/dist/core/plugins/impl/replay.d.mts +0 -9
  115. package/dist/core/plugins/impl/replay.d.mts.map +0 -1
  116. package/dist/core/plugins/impl/replay.mjs +0 -144
  117. package/dist/core/plugins/impl/replay.mjs.map +0 -1
  118. package/dist/core/plugins/impl/server-tracing.d.mts +0 -7
  119. package/dist/core/plugins/impl/server-tracing.d.mts.map +0 -1
  120. package/dist/core/plugins/impl/server-tracing.mjs +0 -160
  121. package/dist/core/plugins/impl/server-tracing.mjs.map +0 -1
  122. package/dist/core/plugins/plugin-event-system.d.mts +0 -47
  123. package/dist/core/plugins/plugin-event-system.d.mts.map +0 -1
  124. package/dist/core/plugins/plugin-event-system.mjs +0 -75
  125. package/dist/core/plugins/plugin-event-system.mjs.map +0 -1
  126. package/dist/core/plugins/plugin-loader.d.mts +0 -22
  127. package/dist/core/plugins/plugin-loader.d.mts.map +0 -1
  128. package/dist/core/plugins/plugin-loader.mjs +0 -142
  129. package/dist/core/plugins/plugin-loader.mjs.map +0 -1
  130. package/dist/core/runtime/config.d.mts +0 -14
  131. package/dist/core/runtime/config.d.mts.map +0 -1
  132. package/dist/core/runtime/config.mjs +0 -39
  133. package/dist/core/runtime/config.mjs.map +0 -1
  134. package/dist/core/runtime/context.d.mts +0 -25
  135. package/dist/core/runtime/context.d.mts.map +0 -1
  136. package/dist/core/runtime/context.mjs +0 -48
  137. package/dist/core/runtime/context.mjs.map +0 -1
  138. package/dist/core/runtime/ingest-target.d.mts +0 -10
  139. package/dist/core/runtime/ingest-target.d.mts.map +0 -1
  140. package/dist/core/runtime/ingest-target.mjs +0 -15
  141. package/dist/core/runtime/ingest-target.mjs.map +0 -1
  142. package/dist/core/schemas.d.mts +0 -85
  143. package/dist/core/schemas.d.mts.map +0 -1
  144. package/dist/core/schemas.mjs +0 -1
  145. package/dist/effect/build-envelope.d.mts +0 -9
  146. package/dist/effect/build-envelope.d.mts.map +0 -1
  147. package/dist/effect/build-envelope.mjs +0 -29
  148. package/dist/effect/build-envelope.mjs.map +0 -1
  149. package/dist/effect/errors.d.mts +0 -36
  150. package/dist/effect/errors.d.mts.map +0 -1
  151. package/dist/effect/errors.mjs +0 -10
  152. package/dist/effect/errors.mjs.map +0 -1
  153. package/dist/effect/layers/config.layer.d.mts +0 -13
  154. package/dist/effect/layers/config.layer.d.mts.map +0 -1
  155. package/dist/effect/layers/config.layer.mjs +0 -21
  156. package/dist/effect/layers/config.layer.mjs.map +0 -1
  157. package/dist/effect/layers/context.layer.d.mts +0 -12
  158. package/dist/effect/layers/context.layer.d.mts.map +0 -1
  159. package/dist/effect/layers/context.layer.mjs +0 -14
  160. package/dist/effect/layers/context.layer.mjs.map +0 -1
  161. package/dist/effect/layers/http.layer.d.mts +0 -21
  162. package/dist/effect/layers/http.layer.d.mts.map +0 -1
  163. package/dist/effect/layers/http.layer.mjs +0 -113
  164. package/dist/effect/layers/http.layer.mjs.map +0 -1
  165. package/dist/effect/layers/queue.layer.d.mts +0 -30
  166. package/dist/effect/layers/queue.layer.d.mts.map +0 -1
  167. package/dist/effect/layers/queue.layer.mjs +0 -232
  168. package/dist/effect/layers/queue.layer.mjs.map +0 -1
  169. package/dist/effect/layers/session.layer.d.mts +0 -26
  170. package/dist/effect/layers/session.layer.d.mts.map +0 -1
  171. package/dist/effect/layers/session.layer.mjs +0 -126
  172. package/dist/effect/layers/session.layer.mjs.map +0 -1
  173. package/dist/effect/layers/storage.layer.d.mts +0 -19
  174. package/dist/effect/layers/storage.layer.d.mts.map +0 -1
  175. package/dist/effect/layers/storage.layer.mjs +0 -200
  176. package/dist/effect/layers/storage.layer.mjs.map +0 -1
  177. package/dist/effect/layers/tracer.layer.d.mts +0 -9
  178. package/dist/effect/layers/tracer.layer.d.mts.map +0 -1
  179. package/dist/effect/layers/tracer.layer.mjs +0 -11
  180. package/dist/effect/layers/tracer.layer.mjs.map +0 -1
  181. package/dist/effect/runtime-services.d.mts +0 -22
  182. package/dist/effect/runtime-services.d.mts.map +0 -1
  183. package/dist/effect/runtime-services.mjs +0 -76
  184. package/dist/effect/runtime-services.mjs.map +0 -1
  185. package/dist/effect/tags.d.mts +0 -50
  186. package/dist/effect/tags.d.mts.map +0 -1
  187. package/dist/effect/tags.mjs +0 -7
  188. package/dist/effect/tags.mjs.map +0 -1
  189. package/dist/hooks/use-runtime-and-plugins.d.mts +0 -7
  190. package/dist/hooks/use-runtime-and-plugins.d.mts.map +0 -1
  191. package/dist/hooks/use-runtime-and-plugins.mjs +0 -153
  192. package/dist/hooks/use-runtime-and-plugins.mjs.map +0 -1
  193. package/dist/hooks/use-session.d.mts +0 -40
  194. package/dist/hooks/use-session.d.mts.map +0 -1
  195. package/dist/hooks/use-session.mjs +0 -96
  196. package/dist/hooks/use-session.mjs.map +0 -1
  197. package/dist/package.mjs +0 -100
  198. package/dist/package.mjs.map +0 -1
  199. package/dist/server/auth.d.mts +0 -11
  200. package/dist/server/auth.d.mts.map +0 -1
  201. package/dist/server/auth.mjs +0 -36
  202. package/dist/server/auth.mjs.map +0 -1
  203. package/dist/server/capture.d.mts +0 -18
  204. package/dist/server/capture.d.mts.map +0 -1
  205. package/dist/server/capture.mjs +0 -105
  206. package/dist/server/capture.mjs.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.mts","names":[],"sources":["../../../src/plugins/lib/types.ts"],"mappings":";;;KAEY,aAAA;AAAA,UAEK,aAAA;EACf,OAAA,WAAkB,SAAA,EAAW,IAAA,EAAM,CAAA,EAAG,OAAA,EAAS,eAAA,CAAgB,CAAA;EAC/D,YAAA;AAAA;AAAA,UAGe,MAAA;EAAA,SACN,IAAA;EACT,KAAA,CAAM,GAAA,EAAK,aAAA,GAAgB,aAAA;AAAA"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import { Plugin } from "./lib/types.mjs";
2
+
3
+ //#region src/plugins/pages.d.ts
4
+ declare const pagesPlugin: Plugin;
5
+ //#endregion
6
+ export { pagesPlugin as default, pagesPlugin };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pages.d.mts","names":[],"sources":["../../src/plugins/pages.ts"],"mappings":";;;cAmDa,WAAA,EAAa,MAAA"}
@@ -0,0 +1,102 @@
1
+ //#region src/plugins/pages.ts
2
+ const INTERACTIVE_TAGS = new Set([
3
+ "A",
4
+ "BUTTON",
5
+ "INPUT",
6
+ "SELECT",
7
+ "TEXTAREA"
8
+ ]);
9
+ const INTERACTIVE_ROLES = new Set([
10
+ "button",
11
+ "link",
12
+ "tab",
13
+ "menuitem",
14
+ "checkbox",
15
+ "radio",
16
+ "switch"
17
+ ]);
18
+ function isInteractive(el) {
19
+ if (INTERACTIVE_TAGS.has(el.tagName)) return true;
20
+ const role = el.getAttribute("role");
21
+ return role !== null && INTERACTIVE_ROLES.has(role);
22
+ }
23
+ function closestInteractive(target) {
24
+ if (!(target instanceof Element)) return null;
25
+ let el = target;
26
+ while (el) {
27
+ if (isInteractive(el)) return el;
28
+ el = el.parentElement;
29
+ }
30
+ return null;
31
+ }
32
+ function elementDescriptor(el) {
33
+ return {
34
+ tag: el.tagName.toLowerCase(),
35
+ text: el.textContent?.trim().slice(0, 120) ?? null,
36
+ ...el.id && { id: el.id },
37
+ ...el.getAttribute("role") && { role: el.getAttribute("role") },
38
+ ...el instanceof HTMLAnchorElement && { href: el.href }
39
+ };
40
+ }
41
+ const pagesPlugin = {
42
+ name: "pages",
43
+ setup(ctx) {
44
+ let pageEnteredAt = Date.now();
45
+ let currentUrl = location.href;
46
+ const emitPageview = () => {
47
+ currentUrl = location.href;
48
+ pageEnteredAt = Date.now();
49
+ ctx.capture("pageview", {
50
+ url: currentUrl,
51
+ title: document.title || void 0
52
+ });
53
+ };
54
+ const emitPageleave = () => {
55
+ ctx.capture("pageleave", {
56
+ url: currentUrl,
57
+ durationMs: Date.now() - pageEnteredAt
58
+ });
59
+ };
60
+ const originalPushState = history.pushState;
61
+ const originalReplaceState = history.replaceState;
62
+ history.pushState = function(...args) {
63
+ emitPageleave();
64
+ originalPushState.apply(this, args);
65
+ emitPageview();
66
+ };
67
+ history.replaceState = function(...args) {
68
+ originalReplaceState.apply(this, args);
69
+ currentUrl = location.href;
70
+ };
71
+ const onPopState = () => {
72
+ emitPageleave();
73
+ emitPageview();
74
+ };
75
+ globalThis.addEventListener("popstate", onPopState);
76
+ const onBeforeUnload = () => {
77
+ emitPageleave();
78
+ };
79
+ globalThis.addEventListener("beforeunload", onBeforeUnload);
80
+ const onVisibilityChange = () => {
81
+ if (document.visibilityState === "hidden") emitPageleave();
82
+ };
83
+ globalThis.addEventListener("visibilitychange", onVisibilityChange);
84
+ const onClick = (event) => {
85
+ const el = closestInteractive(event.target);
86
+ if (!el) return;
87
+ ctx.capture("ui_event", { event: elementDescriptor(el) });
88
+ };
89
+ document.addEventListener("click", onClick, { capture: true });
90
+ emitPageview();
91
+ return () => {
92
+ history.pushState = originalPushState;
93
+ history.replaceState = originalReplaceState;
94
+ globalThis.removeEventListener("popstate", onPopState);
95
+ globalThis.removeEventListener("beforeunload", onBeforeUnload);
96
+ globalThis.removeEventListener("visibilitychange", onVisibilityChange);
97
+ document.removeEventListener("click", onClick, { capture: true });
98
+ };
99
+ }
100
+ };
101
+ //#endregion
102
+ export { pagesPlugin as default, pagesPlugin };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pages.mjs","names":[],"sources":["../../src/plugins/pages.ts"],"sourcesContent":["import type { Plugin } from \"./lib/types.js\";\n\nconst INTERACTIVE_TAGS = new Set([\n \"A\",\n \"BUTTON\",\n \"INPUT\",\n \"SELECT\",\n \"TEXTAREA\",\n]);\nconst INTERACTIVE_ROLES = new Set([\n \"button\",\n \"link\",\n \"tab\",\n \"menuitem\",\n \"checkbox\",\n \"radio\",\n \"switch\",\n]);\n\nfunction isInteractive(el: Element): boolean {\n if (INTERACTIVE_TAGS.has(el.tagName)) {\n return true;\n }\n const role = el.getAttribute(\"role\");\n return role !== null && INTERACTIVE_ROLES.has(role);\n}\n\nfunction closestInteractive(target: EventTarget | null): Element | null {\n if (!(target instanceof Element)) {\n return null;\n }\n let el: Element | null = target;\n while (el) {\n if (isInteractive(el)) {\n return el;\n }\n el = el.parentElement;\n }\n return null;\n}\n\nfunction elementDescriptor(el: Element): Record<string, unknown> {\n return {\n tag: el.tagName.toLowerCase(),\n text: el.textContent?.trim().slice(0, 120) ?? null,\n ...(el.id && { id: el.id }),\n ...(el.getAttribute(\"role\") && { role: el.getAttribute(\"role\") }),\n ...(el instanceof HTMLAnchorElement && { href: el.href }),\n };\n}\n\nexport const pagesPlugin: Plugin = {\n name: \"pages\",\n\n setup(ctx) {\n let pageEnteredAt = Date.now();\n let currentUrl = location.href;\n\n const emitPageview = () => {\n currentUrl = location.href;\n pageEnteredAt = Date.now();\n ctx.capture(\"pageview\", {\n url: currentUrl,\n title: document.title || undefined,\n });\n };\n\n const emitPageleave = () => {\n ctx.capture(\"pageleave\", {\n url: currentUrl,\n durationMs: Date.now() - pageEnteredAt,\n });\n };\n\n // Patch history API for SPA navigation\n const originalPushState = history.pushState;\n const originalReplaceState = history.replaceState;\n\n history.pushState = function (...args) {\n emitPageleave();\n originalPushState.apply(this, args);\n emitPageview();\n };\n\n history.replaceState = function (...args) {\n originalReplaceState.apply(this, args);\n // replaceState doesn't create a new entry — update URL tracking but skip pageview\n currentUrl = location.href;\n };\n\n const onPopState = () => {\n emitPageleave();\n emitPageview();\n };\n globalThis.addEventListener(\"popstate\", onPopState);\n\n const onBeforeUnload = () => {\n emitPageleave();\n };\n globalThis.addEventListener(\"beforeunload\", onBeforeUnload);\n\n const onVisibilityChange = () => {\n if (document.visibilityState === \"hidden\") {\n emitPageleave();\n }\n };\n globalThis.addEventListener(\"visibilitychange\", onVisibilityChange);\n\n const onClick = (event: MouseEvent) => {\n const el = closestInteractive(event.target);\n if (!el) {\n return;\n }\n ctx.capture(\"ui_event\", { event: elementDescriptor(el) });\n };\n document.addEventListener(\"click\", onClick, { capture: true });\n\n // Initial pageview\n emitPageview();\n\n return () => {\n history.pushState = originalPushState;\n history.replaceState = originalReplaceState;\n globalThis.removeEventListener(\"popstate\", onPopState);\n globalThis.removeEventListener(\"beforeunload\", onBeforeUnload);\n globalThis.removeEventListener(\"visibilitychange\", onVisibilityChange);\n document.removeEventListener(\"click\", onClick, { capture: true });\n };\n },\n};\n\nexport default pagesPlugin;\n"],"mappings":";AAEA,MAAM,mBAAmB,IAAI,IAAI;CAC/B;CACA;CACA;CACA;CACA;CACD,CAAC;AACF,MAAM,oBAAoB,IAAI,IAAI;CAChC;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AAEF,SAAS,cAAc,IAAsB;AAC3C,KAAI,iBAAiB,IAAI,GAAG,QAAQ,CAClC,QAAO;CAET,MAAM,OAAO,GAAG,aAAa,OAAO;AACpC,QAAO,SAAS,QAAQ,kBAAkB,IAAI,KAAK;;AAGrD,SAAS,mBAAmB,QAA4C;AACtE,KAAI,EAAE,kBAAkB,SACtB,QAAO;CAET,IAAI,KAAqB;AACzB,QAAO,IAAI;AACT,MAAI,cAAc,GAAG,CACnB,QAAO;AAET,OAAK,GAAG;;AAEV,QAAO;;AAGT,SAAS,kBAAkB,IAAsC;AAC/D,QAAO;EACL,KAAK,GAAG,QAAQ,aAAa;EAC7B,MAAM,GAAG,aAAa,MAAM,CAAC,MAAM,GAAG,IAAI,IAAI;EAC9C,GAAI,GAAG,MAAM,EAAE,IAAI,GAAG,IAAI;EAC1B,GAAI,GAAG,aAAa,OAAO,IAAI,EAAE,MAAM,GAAG,aAAa,OAAO,EAAE;EAChE,GAAI,cAAc,qBAAqB,EAAE,MAAM,GAAG,MAAM;EACzD;;AAGH,MAAa,cAAsB;CACjC,MAAM;CAEN,MAAM,KAAK;EACT,IAAI,gBAAgB,KAAK,KAAK;EAC9B,IAAI,aAAa,SAAS;EAE1B,MAAM,qBAAqB;AACzB,gBAAa,SAAS;AACtB,mBAAgB,KAAK,KAAK;AAC1B,OAAI,QAAQ,YAAY;IACtB,KAAK;IACL,OAAO,SAAS,SAAS,KAAA;IAC1B,CAAC;;EAGJ,MAAM,sBAAsB;AAC1B,OAAI,QAAQ,aAAa;IACvB,KAAK;IACL,YAAY,KAAK,KAAK,GAAG;IAC1B,CAAC;;EAIJ,MAAM,oBAAoB,QAAQ;EAClC,MAAM,uBAAuB,QAAQ;AAErC,UAAQ,YAAY,SAAU,GAAG,MAAM;AACrC,kBAAe;AACf,qBAAkB,MAAM,MAAM,KAAK;AACnC,iBAAc;;AAGhB,UAAQ,eAAe,SAAU,GAAG,MAAM;AACxC,wBAAqB,MAAM,MAAM,KAAK;AAEtC,gBAAa,SAAS;;EAGxB,MAAM,mBAAmB;AACvB,kBAAe;AACf,iBAAc;;AAEhB,aAAW,iBAAiB,YAAY,WAAW;EAEnD,MAAM,uBAAuB;AAC3B,kBAAe;;AAEjB,aAAW,iBAAiB,gBAAgB,eAAe;EAE3D,MAAM,2BAA2B;AAC/B,OAAI,SAAS,oBAAoB,SAC/B,gBAAe;;AAGnB,aAAW,iBAAiB,oBAAoB,mBAAmB;EAEnE,MAAM,WAAW,UAAsB;GACrC,MAAM,KAAK,mBAAmB,MAAM,OAAO;AAC3C,OAAI,CAAC,GACH;AAEF,OAAI,QAAQ,YAAY,EAAE,OAAO,kBAAkB,GAAG,EAAE,CAAC;;AAE3D,WAAS,iBAAiB,SAAS,SAAS,EAAE,SAAS,MAAM,CAAC;AAG9D,gBAAc;AAEd,eAAa;AACX,WAAQ,YAAY;AACpB,WAAQ,eAAe;AACvB,cAAW,oBAAoB,YAAY,WAAW;AACtD,cAAW,oBAAoB,gBAAgB,eAAe;AAC9D,cAAW,oBAAoB,oBAAoB,mBAAmB;AACtE,YAAS,oBAAoB,SAAS,SAAS,EAAE,SAAS,MAAM,CAAC;;;CAGtE"}
@@ -0,0 +1,6 @@
1
+ import { Plugin } from "./lib/types.mjs";
2
+
3
+ //#region src/plugins/rage-clicks.d.ts
4
+ declare const rageClicksPlugin: Plugin;
5
+ //#endregion
6
+ export { rageClicksPlugin as default, rageClicksPlugin };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rage-clicks.d.mts","names":[],"sources":["../../src/plugins/rage-clicks.ts"],"mappings":";;;cA6Ba,gBAAA,EAAkB,MAAA"}
@@ -0,0 +1,53 @@
1
+ //#region src/plugins/rage-clicks.ts
2
+ const CLICK_THRESHOLD = 3;
3
+ const TIME_WINDOW_MS = 800;
4
+ const PROXIMITY_PX = 30;
5
+ function distance(a, b) {
6
+ return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);
7
+ }
8
+ function selectorFor(el) {
9
+ if (!el) return "unknown";
10
+ if (el.id) return `#${el.id}`;
11
+ const classes = [...el.classList].slice(0, 3).join(".");
12
+ const tag = el.tagName.toLowerCase();
13
+ return classes ? `${tag}.${classes}` : tag;
14
+ }
15
+ const rageClicksPlugin = {
16
+ name: "rage-clicks",
17
+ setup(ctx) {
18
+ const clicks = [];
19
+ const onClick = (event) => {
20
+ const now = Date.now();
21
+ clicks.push({
22
+ x: event.clientX,
23
+ y: event.clientY,
24
+ ts: now,
25
+ target: event.target instanceof Element ? event.target : null
26
+ });
27
+ while (clicks.length > 0 && now - (clicks[0]?.ts ?? 0) > TIME_WINDOW_MS) clicks.shift();
28
+ if (clicks.length < CLICK_THRESHOLD) return;
29
+ const anchor = clicks[0];
30
+ if (!anchor) return;
31
+ const clustered = clicks.filter((c) => distance(anchor, c) <= PROXIMITY_PX);
32
+ if (clustered.length < CLICK_THRESHOLD) return;
33
+ const last = clustered.at(-1);
34
+ if (!last) return;
35
+ ctx.capture("rage_click", {
36
+ count: clustered.length,
37
+ timeWindow: last.ts - anchor.ts,
38
+ selector: selectorFor(last.target),
39
+ text: last.target?.textContent?.trim().slice(0, 120) ?? "",
40
+ x: last.x,
41
+ y: last.y,
42
+ timestamp: now
43
+ });
44
+ clicks.length = 0;
45
+ };
46
+ document.addEventListener("click", onClick, { capture: true });
47
+ return () => {
48
+ document.removeEventListener("click", onClick, { capture: true });
49
+ };
50
+ }
51
+ };
52
+ //#endregion
53
+ export { rageClicksPlugin as default, rageClicksPlugin };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rage-clicks.mjs","names":[],"sources":["../../src/plugins/rage-clicks.ts"],"sourcesContent":["import type { Plugin } from \"./lib/types.js\";\n\nconst CLICK_THRESHOLD = 3;\nconst TIME_WINDOW_MS = 800;\nconst PROXIMITY_PX = 30;\n\ninterface Click {\n target: Element | null;\n ts: number;\n x: number;\n y: number;\n}\n\nfunction distance(a: Click, b: Click): number {\n return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);\n}\n\nfunction selectorFor(el: Element | null): string {\n if (!el) {\n return \"unknown\";\n }\n if (el.id) {\n return `#${el.id}`;\n }\n const classes = [...el.classList].slice(0, 3).join(\".\");\n const tag = el.tagName.toLowerCase();\n return classes ? `${tag}.${classes}` : tag;\n}\n\nexport const rageClicksPlugin: Plugin = {\n name: \"rage-clicks\",\n\n setup(ctx) {\n const clicks: Click[] = [];\n\n const onClick = (event: MouseEvent) => {\n const now = Date.now();\n clicks.push({\n x: event.clientX,\n y: event.clientY,\n ts: now,\n target: event.target instanceof Element ? event.target : null,\n });\n\n // Prune stale clicks\n while (clicks.length > 0 && now - (clicks[0]?.ts ?? 0) > TIME_WINDOW_MS) {\n clicks.shift();\n }\n\n if (clicks.length < CLICK_THRESHOLD) {\n return;\n }\n\n // Check proximity — all clicks within PROXIMITY_PX of the first\n const anchor = clicks[0];\n if (!anchor) {\n return;\n }\n const clustered = clicks.filter(\n (c) => distance(anchor, c) <= PROXIMITY_PX\n );\n if (clustered.length < CLICK_THRESHOLD) {\n return;\n }\n\n const last = clustered.at(-1);\n if (!last) {\n return;\n }\n ctx.capture(\"rage_click\", {\n count: clustered.length,\n timeWindow: last.ts - anchor.ts,\n selector: selectorFor(last.target),\n text: last.target?.textContent?.trim().slice(0, 120) ?? \"\",\n x: last.x,\n y: last.y,\n timestamp: now,\n });\n\n clicks.length = 0;\n };\n\n document.addEventListener(\"click\", onClick, { capture: true });\n\n return () => {\n document.removeEventListener(\"click\", onClick, { capture: true });\n };\n },\n};\n\nexport default rageClicksPlugin;\n"],"mappings":";AAEA,MAAM,kBAAkB;AACxB,MAAM,iBAAiB;AACvB,MAAM,eAAe;AASrB,SAAS,SAAS,GAAU,GAAkB;AAC5C,QAAO,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE;;AAGvD,SAAS,YAAY,IAA4B;AAC/C,KAAI,CAAC,GACH,QAAO;AAET,KAAI,GAAG,GACL,QAAO,IAAI,GAAG;CAEhB,MAAM,UAAU,CAAC,GAAG,GAAG,UAAU,CAAC,MAAM,GAAG,EAAE,CAAC,KAAK,IAAI;CACvD,MAAM,MAAM,GAAG,QAAQ,aAAa;AACpC,QAAO,UAAU,GAAG,IAAI,GAAG,YAAY;;AAGzC,MAAa,mBAA2B;CACtC,MAAM;CAEN,MAAM,KAAK;EACT,MAAM,SAAkB,EAAE;EAE1B,MAAM,WAAW,UAAsB;GACrC,MAAM,MAAM,KAAK,KAAK;AACtB,UAAO,KAAK;IACV,GAAG,MAAM;IACT,GAAG,MAAM;IACT,IAAI;IACJ,QAAQ,MAAM,kBAAkB,UAAU,MAAM,SAAS;IAC1D,CAAC;AAGF,UAAO,OAAO,SAAS,KAAK,OAAO,OAAO,IAAI,MAAM,KAAK,eACvD,QAAO,OAAO;AAGhB,OAAI,OAAO,SAAS,gBAClB;GAIF,MAAM,SAAS,OAAO;AACtB,OAAI,CAAC,OACH;GAEF,MAAM,YAAY,OAAO,QACtB,MAAM,SAAS,QAAQ,EAAE,IAAI,aAC/B;AACD,OAAI,UAAU,SAAS,gBACrB;GAGF,MAAM,OAAO,UAAU,GAAG,GAAG;AAC7B,OAAI,CAAC,KACH;AAEF,OAAI,QAAQ,cAAc;IACxB,OAAO,UAAU;IACjB,YAAY,KAAK,KAAK,OAAO;IAC7B,UAAU,YAAY,KAAK,OAAO;IAClC,MAAM,KAAK,QAAQ,aAAa,MAAM,CAAC,MAAM,GAAG,IAAI,IAAI;IACxD,GAAG,KAAK;IACR,GAAG,KAAK;IACR,WAAW;IACZ,CAAC;AAEF,UAAO,SAAS;;AAGlB,WAAS,iBAAiB,SAAS,SAAS,EAAE,SAAS,MAAM,CAAC;AAE9D,eAAa;AACX,YAAS,oBAAoB,SAAS,SAAS,EAAE,SAAS,MAAM,CAAC;;;CAGtE"}
@@ -0,0 +1,6 @@
1
+ import { Plugin } from "./lib/types.mjs";
2
+
3
+ //#region src/plugins/replay.d.ts
4
+ declare const replayPlugin: Plugin;
5
+ //#endregion
6
+ export { replayPlugin as default, replayPlugin };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replay.d.mts","names":[],"sources":["../../src/plugins/replay.ts"],"mappings":";;;cAOa,YAAA,EAAc,MAAA"}
@@ -0,0 +1,62 @@
1
+ import { createLogger } from "../util/log.mjs";
2
+ //#region src/plugins/replay.ts
3
+ const log = createLogger("replay");
4
+ const FLUSH_INTERVAL_MS = 1e4;
5
+ const replayPlugin = {
6
+ name: "replay",
7
+ setup(ctx) {
8
+ let stopFn = null;
9
+ let events = [];
10
+ let flushTimer = null;
11
+ let firstTs = null;
12
+ let lastTs = null;
13
+ const flush = () => {
14
+ if (events.length === 0) return;
15
+ const chunk = events;
16
+ events = [];
17
+ const fts = firstTs;
18
+ const lts = lastTs;
19
+ firstTs = null;
20
+ lastTs = null;
21
+ ctx.capture("replay_chunk", {
22
+ ts: Date.now(),
23
+ count: chunk.length,
24
+ events: chunk,
25
+ ...fts !== null && { first_event_ts: fts },
26
+ ...lts !== null && { last_event_ts: lts }
27
+ });
28
+ };
29
+ const onVisibilityChange = () => {
30
+ if (document.visibilityState === "hidden") flush();
31
+ };
32
+ const onBeforeUnload = () => {
33
+ flush();
34
+ };
35
+ const init = async () => {
36
+ try {
37
+ stopFn = (await import("rrweb")).record({ emit(event) {
38
+ const ts = Date.now();
39
+ if (firstTs === null) firstTs = ts;
40
+ lastTs = ts;
41
+ events.push(JSON.stringify(event));
42
+ } }) ?? null;
43
+ flushTimer = setInterval(flush, FLUSH_INTERVAL_MS);
44
+ globalThis.addEventListener("visibilitychange", onVisibilityChange);
45
+ globalThis.addEventListener("beforeunload", onBeforeUnload);
46
+ log.debug("recording started");
47
+ } catch {
48
+ log.error("rrweb failed to load, replay disabled");
49
+ }
50
+ };
51
+ init().catch(() => {});
52
+ return () => {
53
+ flush();
54
+ stopFn?.();
55
+ if (flushTimer) clearInterval(flushTimer);
56
+ globalThis.removeEventListener("visibilitychange", onVisibilityChange);
57
+ globalThis.removeEventListener("beforeunload", onBeforeUnload);
58
+ };
59
+ }
60
+ };
61
+ //#endregion
62
+ export { replayPlugin as default, replayPlugin };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replay.mjs","names":[],"sources":["../../src/plugins/replay.ts"],"sourcesContent":["import { createLogger } from \"../util/log.js\";\nimport type { Plugin } from \"./lib/types.js\";\n\nconst log = createLogger(\"replay\");\n\nconst FLUSH_INTERVAL_MS = 10_000;\n\nexport const replayPlugin: Plugin = {\n name: \"replay\",\n\n setup(ctx) {\n let stopFn: (() => void) | null = null;\n let events: string[] = [];\n let flushTimer: ReturnType<typeof setInterval> | null = null;\n let firstTs: number | null = null;\n let lastTs: number | null = null;\n\n const flush = () => {\n if (events.length === 0) {\n return;\n }\n const chunk = events;\n events = [];\n const fts = firstTs;\n const lts = lastTs;\n firstTs = null;\n lastTs = null;\n\n ctx.capture(\"replay_chunk\", {\n ts: Date.now(),\n count: chunk.length,\n events: chunk,\n ...(fts !== null && { first_event_ts: fts }),\n ...(lts !== null && { last_event_ts: lts }),\n });\n };\n\n const onVisibilityChange = () => {\n if (document.visibilityState === \"hidden\") {\n flush();\n }\n };\n const onBeforeUnload = () => {\n flush();\n };\n\n const init = async () => {\n try {\n const rrweb = await import(\"rrweb\");\n stopFn =\n rrweb.record({\n emit(event) {\n const ts = Date.now();\n if (firstTs === null) {\n firstTs = ts;\n }\n lastTs = ts;\n events.push(JSON.stringify(event));\n },\n }) ?? null;\n\n flushTimer = setInterval(flush, FLUSH_INTERVAL_MS);\n globalThis.addEventListener(\"visibilitychange\", onVisibilityChange);\n globalThis.addEventListener(\"beforeunload\", onBeforeUnload);\n log.debug(\"recording started\");\n } catch {\n log.error(\"rrweb failed to load, replay disabled\");\n }\n };\n\n init().catch(() => {\n // rrweb load failure is non-fatal\n });\n\n return () => {\n flush();\n stopFn?.();\n if (flushTimer) {\n clearInterval(flushTimer);\n }\n globalThis.removeEventListener(\"visibilitychange\", onVisibilityChange);\n globalThis.removeEventListener(\"beforeunload\", onBeforeUnload);\n };\n },\n};\n\nexport default replayPlugin;\n"],"mappings":";;AAGA,MAAM,MAAM,aAAa,SAAS;AAElC,MAAM,oBAAoB;AAE1B,MAAa,eAAuB;CAClC,MAAM;CAEN,MAAM,KAAK;EACT,IAAI,SAA8B;EAClC,IAAI,SAAmB,EAAE;EACzB,IAAI,aAAoD;EACxD,IAAI,UAAyB;EAC7B,IAAI,SAAwB;EAE5B,MAAM,cAAc;AAClB,OAAI,OAAO,WAAW,EACpB;GAEF,MAAM,QAAQ;AACd,YAAS,EAAE;GACX,MAAM,MAAM;GACZ,MAAM,MAAM;AACZ,aAAU;AACV,YAAS;AAET,OAAI,QAAQ,gBAAgB;IAC1B,IAAI,KAAK,KAAK;IACd,OAAO,MAAM;IACb,QAAQ;IACR,GAAI,QAAQ,QAAQ,EAAE,gBAAgB,KAAK;IAC3C,GAAI,QAAQ,QAAQ,EAAE,eAAe,KAAK;IAC3C,CAAC;;EAGJ,MAAM,2BAA2B;AAC/B,OAAI,SAAS,oBAAoB,SAC/B,QAAO;;EAGX,MAAM,uBAAuB;AAC3B,UAAO;;EAGT,MAAM,OAAO,YAAY;AACvB,OAAI;AAEF,cADc,MAAM,OAAO,UAEnB,OAAO,EACX,KAAK,OAAO;KACV,MAAM,KAAK,KAAK,KAAK;AACrB,SAAI,YAAY,KACd,WAAU;AAEZ,cAAS;AACT,YAAO,KAAK,KAAK,UAAU,MAAM,CAAC;OAErC,CAAC,IAAI;AAER,iBAAa,YAAY,OAAO,kBAAkB;AAClD,eAAW,iBAAiB,oBAAoB,mBAAmB;AACnE,eAAW,iBAAiB,gBAAgB,eAAe;AAC3D,QAAI,MAAM,oBAAoB;WACxB;AACN,QAAI,MAAM,wCAAwC;;;AAItD,QAAM,CAAC,YAAY,GAEjB;AAEF,eAAa;AACX,UAAO;AACP,aAAU;AACV,OAAI,WACF,eAAc,WAAW;AAE3B,cAAW,oBAAoB,oBAAoB,mBAAmB;AACtE,cAAW,oBAAoB,gBAAgB,eAAe;;;CAGnE"}
@@ -1,17 +1,23 @@
1
- import { InterfereProviderValue, Plugin } from "./core/schemas.mjs";
2
- import { Config } from "@interfere/types/sdk/config";
3
1
  import { PropsWithChildren, ReactNode } from "react";
2
+ import { EnvelopePayload, EventType } from "@interfere/types/sdk/envelope";
3
+ import { IdentifyParams } from "@interfere/types/sdk/identify";
4
4
 
5
5
  //#region src/provider.d.ts
6
- interface InterfereProviderProps extends PropsWithChildren {
7
- config: Config;
8
- plugins?: Plugin[];
6
+ interface InterfereContextValue {
7
+ capture<T extends EventType>(type: T, payload: EnvelopePayload<T>): void;
8
+ identity: {
9
+ get(): IdentifyParams | null;
10
+ set(params: IdentifyParams): void;
11
+ };
12
+ session: {
13
+ getId(): string | null;
14
+ getWindowId(): string | null;
15
+ };
9
16
  }
10
17
  declare function InterfereProvider({
11
- children,
12
- config,
13
- plugins
14
- }: InterfereProviderProps): ReactNode;
15
- declare function useInterfere(): InterfereProviderValue;
18
+ children
19
+ }: PropsWithChildren): ReactNode;
20
+ declare function useInterfere(): InterfereContextValue;
21
+ declare function useSession(): string | null;
16
22
  //#endregion
17
- export { InterfereProvider, InterfereProviderProps, useInterfere };
23
+ export { InterfereProvider, useInterfere, useSession };
@@ -1 +1 @@
1
- {"version":3,"file":"provider.d.mts","names":[],"sources":["../src/provider.tsx"],"sourcesContent":[],"mappings":";;;;;UAwBiB,sBAAA,SAA+B;UACtC;EADO,OAAA,CAAA,EAEL,MAFK,EAAA;;AAEL,iBAGI,iBAAA,CAHJ;EAAA,QAAA;EAAA,MAAA;EAAA;AAAA,CAAA,EAOT,sBAPS,CAAA,EAOgB,SAPhB;AAFoC,iBA2BhC,YAAA,CAAA,CA3BgC,EA2BhB,sBA3BgB"}
1
+ {"version":3,"file":"provider.d.mts","names":[],"sources":["../src/provider.tsx"],"mappings":";;;;;UAeU,qBAAA;EACR,OAAA,WAAkB,SAAA,EAAW,IAAA,EAAM,CAAA,EAAG,OAAA,EAAS,eAAA,CAAgB,CAAA;EAC/D,QAAA;IACE,GAAA,IAAO,cAAA;IACP,GAAA,CAAI,MAAA,EAAQ,cAAA;EAAA;EAEd,OAAA;IACE,KAAA;IACA,WAAA;EAAA;AAAA;AAAA,iBAMY,iBAAA,CAAA;EAAoB;AAAA,GAAY,iBAAA,GAAoB,SAAA;AAAA,iBAUpD,YAAA,CAAA,GAAgB,qBAAA;AAAA,iBAQhB,UAAA,CAAA"}
package/dist/provider.mjs CHANGED
@@ -1,26 +1,27 @@
1
1
  "use client";
2
-
3
- import { capture } from "./client.mjs";
4
- import { useRuntimeAndPlugins } from "./hooks/use-runtime-and-plugins.mjs";
5
- import { createContext, useContext, useMemo } from "react";
2
+ import { identity, session } from "./tracking/api.mjs";
3
+ import { capture } from "./internal/client.mjs";
4
+ import { createContext, useContext } from "react";
6
5
  import { jsx } from "react/jsx-runtime";
7
-
8
6
  //#region src/provider.tsx
9
- const InterfereContext = createContext({ capture });
10
- function InterfereProvider({ children, config, plugins }) {
11
- const pluginApis = useRuntimeAndPlugins(config, plugins);
12
- const contextValue = useMemo(() => ({
13
- capture,
14
- ...pluginApis
15
- }), [pluginApis]);
16
- return /* @__PURE__ */ jsx(InterfereContext.Provider, {
17
- value: contextValue,
7
+ const InterfereContext = createContext(null);
8
+ function InterfereProvider({ children }) {
9
+ return /* @__PURE__ */ jsx(InterfereContext, {
10
+ value: {
11
+ capture,
12
+ identity,
13
+ session
14
+ },
18
15
  children
19
16
  });
20
17
  }
21
18
  function useInterfere() {
22
- return useContext(InterfereContext);
19
+ const ctx = useContext(InterfereContext);
20
+ if (!ctx) throw new Error("useInterfere must be used within <InterfereProvider>");
21
+ return ctx;
22
+ }
23
+ function useSession() {
24
+ return useInterfere().session.getId();
23
25
  }
24
-
25
26
  //#endregion
26
- export { InterfereProvider, useInterfere };
27
+ export { InterfereProvider, useInterfere, useSession };
@@ -1 +1 @@
1
- {"version":3,"file":"provider.mjs","names":[],"sources":["../src/provider.tsx"],"sourcesContent":["\"use client\";\n\nimport type { Config } from \"@interfere/types/sdk/config\";\n\nimport {\n createContext,\n type PropsWithChildren,\n type ReactNode,\n useContext,\n useMemo,\n} from \"react\";\n\nimport { capture } from \"./client.js\";\nimport type { InterfereProviderValue, Plugin } from \"./core/schemas.js\";\nimport { useRuntimeAndPlugins } from \"./hooks/use-runtime-and-plugins.js\";\n\nconst interfereContextDefaultValue: InterfereProviderValue = {\n capture,\n};\n\nconst InterfereContext = createContext<InterfereProviderValue>(\n interfereContextDefaultValue\n);\n\nexport interface InterfereProviderProps extends PropsWithChildren {\n config: Config;\n plugins?: Plugin[];\n}\n\nexport function InterfereProvider({\n children,\n config,\n plugins,\n}: InterfereProviderProps): ReactNode {\n const pluginApis = useRuntimeAndPlugins(config, plugins);\n\n const contextValue = useMemo(\n () => ({\n capture,\n ...pluginApis,\n }),\n [pluginApis]\n );\n\n return (\n <InterfereContext.Provider value={contextValue}>\n {children}\n </InterfereContext.Provider>\n );\n}\n\nexport function useInterfere(): InterfereProviderValue {\n const context = useContext(InterfereContext);\n\n return context;\n}\n"],"mappings":";;;;;;;;AAoBA,MAAM,mBAAmB,cAJoC,EAC3D,SACD,CAIA;AAOD,SAAgB,kBAAkB,EAChC,UACA,QACA,WACoC;CACpC,MAAM,aAAa,qBAAqB,QAAQ,QAAQ;CAExD,MAAM,eAAe,eACZ;EACL;EACA,GAAG;EACJ,GACD,CAAC,WAAW,CACb;AAED,QACE,oBAAC,iBAAiB;EAAS,OAAO;EAC/B;GACyB;;AAIhC,SAAgB,eAAuC;AAGrD,QAFgB,WAAW,iBAAiB"}
1
+ {"version":3,"file":"provider.mjs","names":[],"sources":["../src/provider.tsx"],"sourcesContent":["\"use client\";\n\nimport type { EnvelopePayload, EventType } from \"@interfere/types/sdk/envelope\";\nimport type { IdentifyParams } from \"@interfere/types/sdk/identify\";\n\nimport {\n createContext,\n type PropsWithChildren,\n type ReactNode,\n useContext,\n} from \"react\";\n\nimport { capture } from \"./internal/client.js\";\nimport { identity, session } from \"./tracking/api.js\";\n\ninterface InterfereContextValue {\n capture<T extends EventType>(type: T, payload: EnvelopePayload<T>): void;\n identity: {\n get(): IdentifyParams | null;\n set(params: IdentifyParams): void;\n };\n session: {\n getId(): string | null;\n getWindowId(): string | null;\n };\n}\n\nconst InterfereContext = createContext<InterfereContextValue | null>(null);\n\nexport function InterfereProvider({ children }: PropsWithChildren): ReactNode {\n const value: InterfereContextValue = {\n capture,\n identity,\n session,\n };\n\n return <InterfereContext value={value}>{children}</InterfereContext>;\n}\n\nexport function useInterfere(): InterfereContextValue {\n const ctx = useContext(InterfereContext);\n if (!ctx) {\n throw new Error(\"useInterfere must be used within <InterfereProvider>\");\n }\n return ctx;\n}\n\nexport function useSession(): string | null {\n return useInterfere().session.getId();\n}\n"],"mappings":";;;;;;AA2BA,MAAM,mBAAmB,cAA4C,KAAK;AAE1E,SAAgB,kBAAkB,EAAE,YAA0C;AAO5E,QAAO,oBAAC,kBAAD;EAAkB,OANY;GACnC;GACA;GACA;GACD;EAEuC;EAA4B,CAAA;;AAGtE,SAAgB,eAAsC;CACpD,MAAM,MAAM,WAAW,iBAAiB;AACxC,KAAI,CAAC,IACH,OAAM,IAAI,MAAM,uDAAuD;AAEzE,QAAO;;AAGT,SAAgB,aAA4B;AAC1C,QAAO,cAAc,CAAC,QAAQ,OAAO"}
@@ -0,0 +1,22 @@
1
+ import { PluginOverrides } from "../plugins/lib/loader.mjs";
2
+ import { IngestTarget } from "../transport/http.mjs";
3
+ import { IdentifyParams } from "@interfere/types/sdk/identify";
4
+
5
+ //#region src/tracking/api.d.ts
6
+ /**
7
+ * Bootstraps the tracking subsystem: creates the session manager,
8
+ * resolves the session target, and optionally starts Fingerprint Pro.
9
+ */
10
+ declare function bootstrap(sessionTarget: IngestTarget, plugins?: PluginOverrides): void;
11
+ declare const session: {
12
+ getId(): string | null;
13
+ getWindowId(): string | null;
14
+ };
15
+ declare const identity: {
16
+ get(): IdentifyParams | null;
17
+ set(params: IdentifyParams): void;
18
+ clear(): void;
19
+ };
20
+ declare function teardown(): void;
21
+ //#endregion
22
+ export { bootstrap, identity, session, teardown };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.d.mts","names":[],"sources":["../../src/tracking/api.ts"],"mappings":";;;;;;;AA4DA;;iBAAgB,SAAA,CACd,aAAA,EAAe,YAAA,EACf,OAAA,GAAU,eAAA;AAAA,cAgBC,OAAA;EAQZ,KAAA;EAAA,WAAA;AAAA;AAAA,cAEY,QAAA;SACJ,cAAA;cAIK,cAAA;;;iBA4BE,QAAA,CAAA"}
@@ -0,0 +1,88 @@
1
+ import { createLogger } from "../util/log.mjs";
2
+ import { resolveFeatures } from "../plugins/lib/loader.mjs";
3
+ import { SessionManager } from "./session.mjs";
4
+ import { getVisitorId, initVisitor, whenVisitorReady } from "./visitor.mjs";
5
+ import { make } from "tctx/traceparent";
6
+ //#region src/tracking/api.ts
7
+ const log = createLogger("tracking");
8
+ let mgr = null;
9
+ let target = null;
10
+ let currentIdentity = null;
11
+ let identifiedSessionId = null;
12
+ function fire(url, method, headers, body) {
13
+ const h = Object.fromEntries(headers.entries());
14
+ h.traceparent = String(make());
15
+ const sid = mgr?.getSessionId();
16
+ if (sid) h["x-interfere-session"] = sid;
17
+ const vid = getVisitorId();
18
+ if (vid) h["x-interfere-visitor"] = vid;
19
+ fetch(url, {
20
+ method,
21
+ headers: h,
22
+ body: JSON.stringify(body),
23
+ keepalive: true
24
+ }).catch(() => {
25
+ log.warn("fire-and-forget %s failed", method);
26
+ });
27
+ }
28
+ async function onRotate(sessionId) {
29
+ if (!target) return;
30
+ const visitorId = await whenVisitorReady();
31
+ log.debug("POST session %s (visitor=%s)", sessionId, visitorId ?? "pending");
32
+ fire(target.url, "POST", target.headers, {
33
+ sessionId,
34
+ visitorId
35
+ });
36
+ }
37
+ /**
38
+ * Bootstraps the tracking subsystem: creates the session manager,
39
+ * resolves the session target, and optionally starts Fingerprint Pro.
40
+ */
41
+ function bootstrap(sessionTarget, plugins) {
42
+ target = sessionTarget;
43
+ if (resolveFeatures(plugins).fingerprint) initVisitor();
44
+ mgr = new SessionManager((id) => {
45
+ onRotate(id).catch(() => {});
46
+ });
47
+ }
48
+ const session = {
49
+ getId() {
50
+ return mgr?.getSessionId() ?? null;
51
+ },
52
+ getWindowId() {
53
+ return mgr?.getWindowId() ?? null;
54
+ }
55
+ };
56
+ const identity = {
57
+ get() {
58
+ return currentIdentity;
59
+ },
60
+ set(params) {
61
+ if (!(mgr && target)) return;
62
+ const sessionId = mgr.getSessionId();
63
+ if (identifiedSessionId === sessionId) {
64
+ log.debug("skipped, already identified for session %s", sessionId);
65
+ return;
66
+ }
67
+ currentIdentity = params;
68
+ identifiedSessionId = sessionId;
69
+ const visitorId = getVisitorId();
70
+ log.info("PUT session %s → user %s", sessionId, params.identifier);
71
+ fire(target.url, "PUT", target.headers, {
72
+ sessionId,
73
+ visitorId,
74
+ ...params
75
+ });
76
+ },
77
+ clear() {
78
+ currentIdentity = null;
79
+ identifiedSessionId = null;
80
+ }
81
+ };
82
+ function teardown() {
83
+ identity.clear();
84
+ mgr = null;
85
+ target = null;
86
+ }
87
+ //#endregion
88
+ export { bootstrap, identity, session, teardown };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.mjs","names":[],"sources":["../../src/tracking/api.ts"],"sourcesContent":["import type { IdentifyParams } from \"@interfere/types/sdk/identify\";\n\nimport { make } from \"tctx/traceparent\";\n\nimport type { PluginOverrides } from \"../plugins/lib/loader.js\";\nimport { resolveFeatures } from \"../plugins/lib/loader.js\";\nimport type { IngestTarget } from \"../transport/http.js\";\nimport { createLogger } from \"../util/log.js\";\nimport { SessionManager } from \"./session.js\";\nimport { getVisitorId, initVisitor, whenVisitorReady } from \"./visitor.js\";\n\nconst log = createLogger(\"tracking\");\n\nlet mgr: SessionManager | null = null;\nlet target: IngestTarget | null = null;\nlet currentIdentity: IdentifyParams | null = null;\nlet identifiedSessionId: string | null = null;\n\nfunction fire(\n url: string,\n method: \"POST\" | \"PUT\",\n headers: Headers,\n body: unknown\n): void {\n const h: Record<string, string> = Object.fromEntries(headers.entries());\n h.traceparent = String(make());\n\n const sid = mgr?.getSessionId();\n if (sid) {\n h[\"x-interfere-session\"] = sid;\n }\n\n const vid = getVisitorId();\n if (vid) {\n h[\"x-interfere-visitor\"] = vid;\n }\n\n fetch(url, {\n method,\n headers: h,\n body: JSON.stringify(body),\n keepalive: true,\n }).catch(() => {\n log.warn(\"fire-and-forget %s failed\", method);\n });\n}\n\nasync function onRotate(sessionId: string): Promise<void> {\n if (!target) {\n return;\n }\n const visitorId = await whenVisitorReady();\n log.debug(\"POST session %s (visitor=%s)\", sessionId, visitorId ?? \"pending\");\n fire(target.url, \"POST\", target.headers, { sessionId, visitorId });\n}\n\n/**\n * Bootstraps the tracking subsystem: creates the session manager,\n * resolves the session target, and optionally starts Fingerprint Pro.\n */\nexport function bootstrap(\n sessionTarget: IngestTarget,\n plugins?: PluginOverrides\n): void {\n target = sessionTarget;\n\n const features = resolveFeatures(plugins);\n if (features.fingerprint) {\n initVisitor();\n }\n\n mgr = new SessionManager((id) => {\n onRotate(id).catch(() => {\n /* best-effort */\n });\n });\n}\n\nexport const session = {\n getId(): string | null {\n return mgr?.getSessionId() ?? null;\n },\n\n getWindowId(): string | null {\n return mgr?.getWindowId() ?? null;\n },\n};\n\nexport const identity = {\n get(): IdentifyParams | null {\n return currentIdentity;\n },\n\n set(params: IdentifyParams): void {\n if (!(mgr && target)) {\n return;\n }\n const sessionId = mgr.getSessionId();\n if (identifiedSessionId === sessionId) {\n log.debug(\"skipped, already identified for session %s\", sessionId);\n return;\n }\n\n currentIdentity = params;\n identifiedSessionId = sessionId;\n\n const visitorId = getVisitorId();\n log.info(\"PUT session %s → user %s\", sessionId, params.identifier);\n fire(target.url, \"PUT\", target.headers, {\n sessionId,\n visitorId,\n ...params,\n });\n },\n\n clear(): void {\n currentIdentity = null;\n identifiedSessionId = null;\n },\n};\n\nexport function teardown(): void {\n identity.clear();\n mgr = null;\n target = null;\n}\n"],"mappings":";;;;;;AAWA,MAAM,MAAM,aAAa,WAAW;AAEpC,IAAI,MAA6B;AACjC,IAAI,SAA8B;AAClC,IAAI,kBAAyC;AAC7C,IAAI,sBAAqC;AAEzC,SAAS,KACP,KACA,QACA,SACA,MACM;CACN,MAAM,IAA4B,OAAO,YAAY,QAAQ,SAAS,CAAC;AACvE,GAAE,cAAc,OAAO,MAAM,CAAC;CAE9B,MAAM,MAAM,KAAK,cAAc;AAC/B,KAAI,IACF,GAAE,yBAAyB;CAG7B,MAAM,MAAM,cAAc;AAC1B,KAAI,IACF,GAAE,yBAAyB;AAG7B,OAAM,KAAK;EACT;EACA,SAAS;EACT,MAAM,KAAK,UAAU,KAAK;EAC1B,WAAW;EACZ,CAAC,CAAC,YAAY;AACb,MAAI,KAAK,6BAA6B,OAAO;GAC7C;;AAGJ,eAAe,SAAS,WAAkC;AACxD,KAAI,CAAC,OACH;CAEF,MAAM,YAAY,MAAM,kBAAkB;AAC1C,KAAI,MAAM,gCAAgC,WAAW,aAAa,UAAU;AAC5E,MAAK,OAAO,KAAK,QAAQ,OAAO,SAAS;EAAE;EAAW;EAAW,CAAC;;;;;;AAOpE,SAAgB,UACd,eACA,SACM;AACN,UAAS;AAGT,KADiB,gBAAgB,QAAQ,CAC5B,YACX,cAAa;AAGf,OAAM,IAAI,gBAAgB,OAAO;AAC/B,WAAS,GAAG,CAAC,YAAY,GAEvB;GACF;;AAGJ,MAAa,UAAU;CACrB,QAAuB;AACrB,SAAO,KAAK,cAAc,IAAI;;CAGhC,cAA6B;AAC3B,SAAO,KAAK,aAAa,IAAI;;CAEhC;AAED,MAAa,WAAW;CACtB,MAA6B;AAC3B,SAAO;;CAGT,IAAI,QAA8B;AAChC,MAAI,EAAE,OAAO,QACX;EAEF,MAAM,YAAY,IAAI,cAAc;AACpC,MAAI,wBAAwB,WAAW;AACrC,OAAI,MAAM,8CAA8C,UAAU;AAClE;;AAGF,oBAAkB;AAClB,wBAAsB;EAEtB,MAAM,YAAY,cAAc;AAChC,MAAI,KAAK,4BAA4B,WAAW,OAAO,WAAW;AAClE,OAAK,OAAO,KAAK,OAAO,OAAO,SAAS;GACtC;GACA;GACA,GAAG;GACJ,CAAC;;CAGJ,QAAc;AACZ,oBAAkB;AAClB,wBAAsB;;CAEzB;AAED,SAAgB,WAAiB;AAC/B,UAAS,OAAO;AAChB,OAAM;AACN,UAAS"}
@@ -0,0 +1,19 @@
1
+ //#region src/tracking/session.d.ts
2
+ type OnRotate = (sessionId: string) => void;
3
+ declare class SessionManager {
4
+ sessionId: string | null;
5
+ windowId: string | null;
6
+ private readonly local;
7
+ private readonly session;
8
+ private readonly onRotate;
9
+ constructor(onRotate?: OnRotate);
10
+ getSessionId(): string;
11
+ getWindowId(): string;
12
+ whenReady(signal?: AbortSignal): Promise<string>;
13
+ private restore;
14
+ private rotate;
15
+ private touch;
16
+ private isExpired;
17
+ }
18
+ //#endregion
19
+ export { OnRotate, SessionManager };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.mts","names":[],"sources":["../../src/tracking/session.ts"],"mappings":";KAqBY,QAAA,IAAY,SAAA;AAAA,cAEX,cAAA;EACX,SAAA;EACA,QAAA;EAAA,iBACiB,KAAA;EAAA,iBACA,OAAA;EAAA,iBACA,QAAA;cAEL,QAAA,GAAW,QAAA;EAOvB,YAAA,CAAA;EAQA,WAAA,CAAA;EAgBA,SAAA,CAAU,MAAA,GAAS,WAAA,GAAc,OAAA;EAAA,QAsBzB,OAAA;EAAA,QAWA,MAAA;EAAA,QAQA,KAAA;EAAA,QAIA,SAAA;AAAA"}
@@ -0,0 +1,92 @@
1
+ import { nanoid } from "nanoid";
2
+ import { v7 } from "uuid";
3
+ //#region src/tracking/session.ts
4
+ const SESSION_ID_KEY = "interfere:session_id";
5
+ const LAST_ACTIVITY_KEY = "interfere:last_activity";
6
+ const WINDOW_ID_KEY = "interfere:window_id";
7
+ const SESSION_TIMEOUT_MS = 1800 * 1e3;
8
+ const POLL_INTERVAL_MS = 250;
9
+ function tryStorage(type) {
10
+ try {
11
+ const s = globalThis[type];
12
+ const key = "__interfere_probe__";
13
+ s.setItem(key, "1");
14
+ s.removeItem(key);
15
+ return s;
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+ var SessionManager = class {
21
+ sessionId = null;
22
+ windowId = null;
23
+ local;
24
+ session;
25
+ onRotate = null;
26
+ constructor(onRotate) {
27
+ this.local = tryStorage("localStorage");
28
+ this.session = tryStorage("sessionStorage");
29
+ this.onRotate = onRotate ?? null;
30
+ this.restore();
31
+ }
32
+ getSessionId() {
33
+ if (this.sessionId && !this.isExpired()) {
34
+ this.touch();
35
+ return this.sessionId;
36
+ }
37
+ return this.rotate();
38
+ }
39
+ getWindowId() {
40
+ if (this.windowId) return this.windowId;
41
+ const stored = this.session?.getItem(WINDOW_ID_KEY);
42
+ if (stored) {
43
+ this.windowId = stored;
44
+ return stored;
45
+ }
46
+ this.windowId = `win_${nanoid(10)}`;
47
+ this.session?.setItem(WINDOW_ID_KEY, this.windowId);
48
+ return this.windowId;
49
+ }
50
+ whenReady(signal) {
51
+ const id = this.sessionId;
52
+ if (id && !this.isExpired()) return Promise.resolve(id);
53
+ return new Promise((resolve, reject) => {
54
+ const check = setInterval(() => {
55
+ if (signal?.aborted) {
56
+ clearInterval(check);
57
+ reject(signal.reason);
58
+ return;
59
+ }
60
+ const current = this.getSessionId();
61
+ if (current) {
62
+ clearInterval(check);
63
+ resolve(current);
64
+ }
65
+ }, POLL_INTERVAL_MS);
66
+ });
67
+ }
68
+ restore() {
69
+ const stored = this.local?.getItem(SESSION_ID_KEY);
70
+ if (stored && !this.isExpired()) {
71
+ this.sessionId = stored;
72
+ this.touch();
73
+ } else this.rotate();
74
+ }
75
+ rotate() {
76
+ this.sessionId = v7();
77
+ this.local?.setItem(SESSION_ID_KEY, this.sessionId);
78
+ this.touch();
79
+ this.onRotate?.(this.sessionId);
80
+ return this.sessionId;
81
+ }
82
+ touch() {
83
+ this.local?.setItem(LAST_ACTIVITY_KEY, String(Date.now()));
84
+ }
85
+ isExpired() {
86
+ const raw = this.local?.getItem(LAST_ACTIVITY_KEY);
87
+ if (!raw) return true;
88
+ return Date.now() - Number(raw) > SESSION_TIMEOUT_MS;
89
+ }
90
+ };
91
+ //#endregion
92
+ export { SessionManager };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.mjs","names":["uuidv7"],"sources":["../../src/tracking/session.ts"],"sourcesContent":["import { nanoid } from \"nanoid\";\nimport { v7 as uuidv7 } from \"uuid\";\n\nconst SESSION_ID_KEY = \"interfere:session_id\";\nconst LAST_ACTIVITY_KEY = \"interfere:last_activity\";\nconst WINDOW_ID_KEY = \"interfere:window_id\";\nconst SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes\nconst POLL_INTERVAL_MS = 250;\n\nfunction tryStorage(type: \"localStorage\" | \"sessionStorage\"): Storage | null {\n try {\n const s = globalThis[type];\n const key = \"__interfere_probe__\";\n s.setItem(key, \"1\");\n s.removeItem(key);\n return s;\n } catch {\n return null;\n }\n}\n\nexport type OnRotate = (sessionId: string) => void;\n\nexport class SessionManager {\n sessionId: string | null = null;\n windowId: string | null = null;\n private readonly local: Storage | null;\n private readonly session: Storage | null;\n private readonly onRotate: OnRotate | null = null;\n\n constructor(onRotate?: OnRotate) {\n this.local = tryStorage(\"localStorage\");\n this.session = tryStorage(\"sessionStorage\");\n this.onRotate = onRotate ?? null;\n this.restore();\n }\n\n getSessionId(): string {\n if (this.sessionId && !this.isExpired()) {\n this.touch();\n return this.sessionId;\n }\n return this.rotate();\n }\n\n getWindowId(): string {\n if (this.windowId) {\n return this.windowId;\n }\n\n const stored = this.session?.getItem(WINDOW_ID_KEY);\n if (stored) {\n this.windowId = stored;\n return stored;\n }\n\n this.windowId = `win_${nanoid(10)}`;\n this.session?.setItem(WINDOW_ID_KEY, this.windowId);\n return this.windowId;\n }\n\n whenReady(signal?: AbortSignal): Promise<string> {\n const id = this.sessionId;\n if (id && !this.isExpired()) {\n return Promise.resolve(id);\n }\n\n return new Promise((resolve, reject) => {\n const check = setInterval(() => {\n if (signal?.aborted) {\n clearInterval(check);\n reject(signal.reason as Error);\n return;\n }\n const current = this.getSessionId();\n if (current) {\n clearInterval(check);\n resolve(current);\n }\n }, POLL_INTERVAL_MS);\n });\n }\n\n private restore(): void {\n const stored = this.local?.getItem(SESSION_ID_KEY);\n\n if (stored && !this.isExpired()) {\n this.sessionId = stored;\n this.touch();\n } else {\n this.rotate();\n }\n }\n\n private rotate(): string {\n this.sessionId = uuidv7();\n this.local?.setItem(SESSION_ID_KEY, this.sessionId);\n this.touch();\n this.onRotate?.(this.sessionId);\n return this.sessionId;\n }\n\n private touch(): void {\n this.local?.setItem(LAST_ACTIVITY_KEY, String(Date.now()));\n }\n\n private isExpired(): boolean {\n const raw = this.local?.getItem(LAST_ACTIVITY_KEY);\n\n if (!raw) {\n return true;\n }\n return Date.now() - Number(raw) > SESSION_TIMEOUT_MS;\n }\n}\n"],"mappings":";;;AAGA,MAAM,iBAAiB;AACvB,MAAM,oBAAoB;AAC1B,MAAM,gBAAgB;AACtB,MAAM,qBAAqB,OAAU;AACrC,MAAM,mBAAmB;AAEzB,SAAS,WAAW,MAAyD;AAC3E,KAAI;EACF,MAAM,IAAI,WAAW;EACrB,MAAM,MAAM;AACZ,IAAE,QAAQ,KAAK,IAAI;AACnB,IAAE,WAAW,IAAI;AACjB,SAAO;SACD;AACN,SAAO;;;AAMX,IAAa,iBAAb,MAA4B;CAC1B,YAA2B;CAC3B,WAA0B;CAC1B;CACA;CACA,WAA6C;CAE7C,YAAY,UAAqB;AAC/B,OAAK,QAAQ,WAAW,eAAe;AACvC,OAAK,UAAU,WAAW,iBAAiB;AAC3C,OAAK,WAAW,YAAY;AAC5B,OAAK,SAAS;;CAGhB,eAAuB;AACrB,MAAI,KAAK,aAAa,CAAC,KAAK,WAAW,EAAE;AACvC,QAAK,OAAO;AACZ,UAAO,KAAK;;AAEd,SAAO,KAAK,QAAQ;;CAGtB,cAAsB;AACpB,MAAI,KAAK,SACP,QAAO,KAAK;EAGd,MAAM,SAAS,KAAK,SAAS,QAAQ,cAAc;AACnD,MAAI,QAAQ;AACV,QAAK,WAAW;AAChB,UAAO;;AAGT,OAAK,WAAW,OAAO,OAAO,GAAG;AACjC,OAAK,SAAS,QAAQ,eAAe,KAAK,SAAS;AACnD,SAAO,KAAK;;CAGd,UAAU,QAAuC;EAC/C,MAAM,KAAK,KAAK;AAChB,MAAI,MAAM,CAAC,KAAK,WAAW,CACzB,QAAO,QAAQ,QAAQ,GAAG;AAG5B,SAAO,IAAI,SAAS,SAAS,WAAW;GACtC,MAAM,QAAQ,kBAAkB;AAC9B,QAAI,QAAQ,SAAS;AACnB,mBAAc,MAAM;AACpB,YAAO,OAAO,OAAgB;AAC9B;;IAEF,MAAM,UAAU,KAAK,cAAc;AACnC,QAAI,SAAS;AACX,mBAAc,MAAM;AACpB,aAAQ,QAAQ;;MAEjB,iBAAiB;IACpB;;CAGJ,UAAwB;EACtB,MAAM,SAAS,KAAK,OAAO,QAAQ,eAAe;AAElD,MAAI,UAAU,CAAC,KAAK,WAAW,EAAE;AAC/B,QAAK,YAAY;AACjB,QAAK,OAAO;QAEZ,MAAK,QAAQ;;CAIjB,SAAyB;AACvB,OAAK,YAAYA,IAAQ;AACzB,OAAK,OAAO,QAAQ,gBAAgB,KAAK,UAAU;AACnD,OAAK,OAAO;AACZ,OAAK,WAAW,KAAK,UAAU;AAC/B,SAAO,KAAK;;CAGd,QAAsB;AACpB,OAAK,OAAO,QAAQ,mBAAmB,OAAO,KAAK,KAAK,CAAC,CAAC;;CAG5D,YAA6B;EAC3B,MAAM,MAAM,KAAK,OAAO,QAAQ,kBAAkB;AAElD,MAAI,CAAC,IACH,QAAO;AAET,SAAO,KAAK,KAAK,GAAG,OAAO,IAAI,GAAG"}