@alpic-ai/insights 1.156.0 → 1.157.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.
@@ -1,24 +1,67 @@
1
1
  import { ReactNode } from "react";
2
+ //#region src/react/analytics-transport.d.ts
3
+ interface AnalyticsEvent {
4
+ name: string;
5
+ timestamp: number;
6
+ message?: string;
7
+ properties?: Record<string, unknown>;
8
+ duration?: number;
9
+ isError?: boolean;
10
+ error?: string;
11
+ }
12
+ //#endregion
2
13
  //#region src/react/analytics-client.d.ts
3
14
  interface CaptureOptions {
4
15
  message?: string;
5
16
  properties?: Record<string, unknown>;
17
+ /** Duration of the operation the event describes, in milliseconds. */
18
+ duration?: number;
19
+ isError?: boolean;
20
+ error?: string;
6
21
  }
22
+ /** Inspect, transform, or drop (return `null`) each event before it is buffered for delivery. */
23
+ type BeforeSend = (event: AnalyticsEvent) => AnalyticsEvent | null;
7
24
  //#endregion
8
25
  //#region src/react/alpic-analytics.d.ts
9
26
  interface Analytics {
10
27
  /** Queue a custom widget event for delivery to Alpic Analytics. */
11
28
  capture: (name: string, options?: CaptureOptions) => void;
12
29
  }
30
+ /** Toggles for the SDK's built-in event capture. Lifecycle and errors are on by default; interactions are opt-in. */
31
+ interface AutoCaptureOptions {
32
+ lifecycle?: boolean;
33
+ errors?: boolean;
34
+ interactions?: boolean;
35
+ }
36
+ interface AlpicAnalyticsProps {
37
+ children?: ReactNode;
38
+ /** Inspect, transform, or drop each event before delivery. Runs synchronously in `capture`. */
39
+ beforeSend?: BeforeSend;
40
+ autoCapture?: AutoCaptureOptions;
41
+ }
13
42
  /**
14
43
  * Provides Alpic Analytics to descendant components and configures itself from the widget host.
15
44
  * Wrap the widget once, then call `useAnalytics()` from components that capture events.
16
45
  * Events captured before configuration is available are buffered automatically.
17
46
  */
18
- declare function AlpicAnalytics({ children }: {
19
- children?: ReactNode;
20
- }): import("react").JSX.Element;
47
+ declare function AlpicAnalytics({ children, beforeSend, autoCapture }: AlpicAnalyticsProps): import("react").JSX.Element;
21
48
  /** Returns the analytics client from the nearest `AlpicAnalytics` provider. */
22
49
  declare function useAnalytics(): Analytics;
23
50
  //#endregion
24
- export { AlpicAnalytics, type Analytics, type CaptureOptions, useAnalytics };
51
+ //#region src/react/auto-capture.d.ts
52
+ /**
53
+ * Reserved auto-capture event names. The `$` prefix marks events emitted by the SDK itself
54
+ * (lifecycle, errors, interactions) so the analytics UI can distinguish them from custom
55
+ * `capture()` calls. Keep in sync with the server-side reserved namespace.
56
+ */
57
+ declare const AUTO_EVENT: {
58
+ readonly loaded: "$loaded";
59
+ readonly visible: "$visible";
60
+ readonly hidden: "$hidden";
61
+ readonly closed: "$closed";
62
+ readonly error: "$error";
63
+ };
64
+ /** Public DOM contract for declarative interaction capture: the attribute value is the event name. */
65
+ declare const INTERACTION_ATTRIBUTE = "data-alpic-event";
66
+ //#endregion
67
+ export { AUTO_EVENT, AlpicAnalytics, type AlpicAnalyticsProps, type Analytics, type AutoCaptureOptions, type BeforeSend, type CaptureOptions, INTERACTION_ATTRIBUTE, useAnalytics };
@@ -1,4 +1,4 @@
1
- import { createContext, useContext, useEffect, useState } from "react";
1
+ import { createContext, useContext, useEffect, useRef, useState } from "react";
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  //#region src/react/analytics-transport.ts
4
4
  function postAnalyticsBatch(ingestUrl, events) {
@@ -12,9 +12,13 @@ function postAnalyticsBatch(ingestUrl, events) {
12
12
  const FLUSH_DELAY_MS = 2e3;
13
13
  var AnalyticsClient = class {
14
14
  events = [];
15
+ beforeSend;
15
16
  stamp = null;
16
17
  flushTimer = null;
17
18
  warnedBufferFull = false;
19
+ constructor(options) {
20
+ this.beforeSend = options?.beforeSend;
21
+ }
18
22
  configure(stamp) {
19
23
  if (this.stamp !== null && !isSameStamp(this.stamp, stamp)) this.flush();
20
24
  this.stamp = stamp;
@@ -34,7 +38,12 @@ var AnalyticsClient = class {
34
38
  };
35
39
  if (options?.message !== void 0) event.message = options.message;
36
40
  if (options?.properties !== void 0) event.properties = options.properties;
37
- this.events.push(event);
41
+ if (options?.duration !== void 0) event.duration = options.duration;
42
+ if (options?.isError !== void 0) event.isError = options.isError;
43
+ if (options?.error !== void 0) event.error = options.error;
44
+ const outgoing = this.beforeSend ? this.beforeSend(event) : event;
45
+ if (outgoing === null) return;
46
+ this.events.push(outgoing);
38
47
  if (this.events.length >= 50) {
39
48
  this.flush();
40
49
  return;
@@ -62,6 +71,88 @@ function isSameStamp(left, right) {
62
71
  return left.distinctId === right.distinctId && left.sessionId === right.sessionId && left.ingestUrl === right.ingestUrl;
63
72
  }
64
73
  //#endregion
74
+ //#region src/react/auto-capture.ts
75
+ /**
76
+ * Reserved auto-capture event names. The `$` prefix marks events emitted by the SDK itself
77
+ * (lifecycle, errors, interactions) so the analytics UI can distinguish them from custom
78
+ * `capture()` calls. Keep in sync with the server-side reserved namespace.
79
+ */
80
+ const AUTO_EVENT = {
81
+ loaded: "$loaded",
82
+ visible: "$visible",
83
+ hidden: "$hidden",
84
+ closed: "$closed",
85
+ error: "$error"
86
+ };
87
+ /** Public DOM contract for declarative interaction capture: the attribute value is the event name. */
88
+ const INTERACTION_ATTRIBUTE = "data-alpic-event";
89
+ const INTERACTION_DATASET_KEY = "alpicEvent";
90
+ const INTERACTION_DATASET_PREFIX = "alpic";
91
+ /**
92
+ * Resolves the nearest `[data-alpic-event]` ancestor of a click target into an event: the
93
+ * attribute value is the name, and every other `data-alpic-*` attribute becomes a property.
94
+ */
95
+ function resolveInteractionEvent(target) {
96
+ if (!(target instanceof Element)) return null;
97
+ const element = target.closest(`[${INTERACTION_ATTRIBUTE}]`);
98
+ if (element === null) return null;
99
+ const name = element.dataset[INTERACTION_DATASET_KEY];
100
+ if (name === void 0 || name === "") return null;
101
+ const properties = {};
102
+ for (const [key, value] of Object.entries(element.dataset)) {
103
+ if (key === INTERACTION_DATASET_KEY || !key.startsWith(INTERACTION_DATASET_PREFIX) || value === void 0) continue;
104
+ const unprefixed = key.slice(5);
105
+ const propertyKey = unprefixed.charAt(0).toLowerCase() + unprefixed.slice(1);
106
+ properties[propertyKey] = value;
107
+ }
108
+ return Object.keys(properties).length > 0 ? {
109
+ name,
110
+ properties
111
+ } : { name };
112
+ }
113
+ /** Captures uncaught errors and unhandled rejections as `$error`, mapped onto the ingest error columns. */
114
+ function installErrorCapture(client) {
115
+ const onError = (event) => {
116
+ client.capture(AUTO_EVENT.error, {
117
+ isError: true,
118
+ error: event.message,
119
+ properties: {
120
+ stack: event.error instanceof Error ? event.error.stack : void 0,
121
+ source: event.filename
122
+ }
123
+ });
124
+ };
125
+ const onRejection = (event) => {
126
+ const reason = event.reason;
127
+ client.capture(AUTO_EVENT.error, {
128
+ isError: true,
129
+ error: reason instanceof Error ? reason.message : String(reason),
130
+ properties: {
131
+ stack: reason instanceof Error ? reason.stack : void 0,
132
+ source: "unhandledrejection"
133
+ }
134
+ });
135
+ };
136
+ window.addEventListener("error", onError);
137
+ window.addEventListener("unhandledrejection", onRejection);
138
+ return () => {
139
+ window.removeEventListener("error", onError);
140
+ window.removeEventListener("unhandledrejection", onRejection);
141
+ };
142
+ }
143
+ /** Captures clicks on `[data-alpic-event]` elements via a single delegated `document` listener. */
144
+ function installInteractionCapture(client) {
145
+ const onClick = (event) => {
146
+ const resolved = resolveInteractionEvent(event.target);
147
+ if (resolved === null) return;
148
+ client.capture(resolved.name, resolved.properties ? { properties: resolved.properties } : void 0);
149
+ };
150
+ document.addEventListener("click", onClick);
151
+ return () => {
152
+ document.removeEventListener("click", onClick);
153
+ };
154
+ }
155
+ //#endregion
65
156
  //#region src/react/stamp.ts
66
157
  const ANALYTICS_META_KEY = "alpic/analytics";
67
158
  function parseAnalyticsStamp(metadata) {
@@ -131,24 +222,49 @@ const AnalyticsContext = createContext(null);
131
222
  * Wrap the widget once, then call `useAnalytics()` from components that capture events.
132
223
  * Events captured before configuration is available are buffered automatically.
133
224
  */
134
- function AlpicAnalytics({ children }) {
135
- const [client] = useState(() => new AnalyticsClient());
225
+ function AlpicAnalytics({ children, beforeSend, autoCapture }) {
226
+ const beforeSendRef = useRef(beforeSend);
227
+ beforeSendRef.current = beforeSend;
228
+ const [client] = useState(() => new AnalyticsClient({ beforeSend: (event) => {
229
+ const transform = beforeSendRef.current;
230
+ return transform ? transform(event) : event;
231
+ } }));
232
+ const lifecycle = autoCapture?.lifecycle ?? true;
233
+ const errors = autoCapture?.errors ?? true;
234
+ const interactions = autoCapture?.interactions ?? false;
136
235
  useEffect(() => {
137
- const unsubscribe = subscribeToAnalyticsStamp((stamp) => {
236
+ return subscribeToAnalyticsStamp((stamp) => {
138
237
  client.configure(stamp);
139
238
  });
140
- const flushWhenHidden = () => {
141
- if (document.visibilityState === "hidden") client.flush();
239
+ }, [client]);
240
+ useEffect(() => {
241
+ if (lifecycle) client.capture(AUTO_EVENT.loaded);
242
+ const onVisibilityChange = () => {
243
+ if (document.visibilityState === "hidden") {
244
+ if (lifecycle) client.capture(AUTO_EVENT.hidden);
245
+ client.flush();
246
+ } else if (lifecycle) client.capture(AUTO_EVENT.visible);
247
+ };
248
+ const onPageHide = () => {
249
+ if (lifecycle) client.capture(AUTO_EVENT.closed);
250
+ client.flush();
142
251
  };
143
- document.addEventListener("visibilitychange", flushWhenHidden);
144
- window.addEventListener("pagehide", client.flush);
252
+ document.addEventListener("visibilitychange", onVisibilityChange);
253
+ window.addEventListener("pagehide", onPageHide);
145
254
  return () => {
146
- unsubscribe();
147
- document.removeEventListener("visibilitychange", flushWhenHidden);
148
- window.removeEventListener("pagehide", client.flush);
255
+ document.removeEventListener("visibilitychange", onVisibilityChange);
256
+ window.removeEventListener("pagehide", onPageHide);
149
257
  client.flush();
150
258
  };
151
- }, [client]);
259
+ }, [client, lifecycle]);
260
+ useEffect(() => {
261
+ if (!errors) return;
262
+ return installErrorCapture(client);
263
+ }, [client, errors]);
264
+ useEffect(() => {
265
+ if (!interactions) return;
266
+ return installInteractionCapture(client);
267
+ }, [client, interactions]);
152
268
  return /* @__PURE__ */ jsx(AnalyticsContext.Provider, {
153
269
  value: client,
154
270
  children
@@ -161,4 +277,4 @@ function useAnalytics() {
161
277
  return analytics;
162
278
  }
163
279
  //#endregion
164
- export { AlpicAnalytics, useAnalytics };
280
+ export { AUTO_EVENT, AlpicAnalytics, INTERACTION_ATTRIBUTE, useAnalytics };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alpic-ai/insights",
3
- "version": "1.156.0",
3
+ "version": "1.157.0",
4
4
  "description": "User insights middlewares for Alpic-hosted MCP servers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",