@indesad/observability-angular 0.0.0-stage → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,67 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # @indesad/observability-angular
2
+
3
+ SDK de navegador para IndeFlow Observabilidad. Captura errores no controlados y los envía a IndeFlow con
4
+ navegador, sistema operativo, URL, release y la secuencia de clics y páginas previa al error.
5
+ No depende de Angular: sirve igual en React, Vue o JavaScript puro.
6
+
7
+ ## Instalación
8
+
9
+ Paquete oficial de INDESAD en npm:
10
+
11
+ ```bash
12
+ npm install @indesad/observability-angular@1.1.1
13
+ ```
14
+
15
+ ## Uso en Angular
16
+
17
+ ```ts
18
+ // app.config.ts
19
+ import { ApplicationConfig, ErrorHandler } from '@angular/core';
20
+ import { createIndeFlowObservabilityClient, IndeFlowErrorHandler } from '@indesad/observability-angular';
21
+
22
+ export const observability = createIndeFlowObservabilityClient({
23
+ sourceToken: 'if_obs_...', // token de la fuente: Proyecto → Observabilidad → fuente → Tokens
24
+ environment: 'production',
25
+ release: 'mi-app@1.4.0'
26
+ });
27
+
28
+ export const appConfig: ApplicationConfig = {
29
+ providers: [{ provide: ErrorHandler, useValue: new IndeFlowErrorHandler(observability) }]
30
+ };
31
+ ```
32
+
33
+ ## Uso sin Angular
34
+
35
+ ```ts
36
+ import { createIndeFlowObservabilityClient } from '@indesad/observability-angular';
37
+
38
+ const observability = createIndeFlowObservabilityClient({ sourceToken: 'if_obs_...', release: 'mi-app@1.4.0' });
39
+
40
+ try { riesgoso(); } catch (error) { observability.captureException(error); }
41
+ ```
42
+
43
+ ## Opciones
44
+
45
+ | Opción | Por defecto | Qué hace |
46
+ | --- | --- | --- |
47
+ | `sourceToken` | — | Token de la fuente. Si llega después del arranque, usa `client.configure({ sourceToken })`. |
48
+ | `ingestEndpoint` | `https://api.indeflow.app/api/v1/ingest/events` | Destino de los eventos. |
49
+ | `environment` | `production` | Ambiente informado en cada evento. |
50
+ | `release` | `1.0.0` | Versión de tu app; permite saber en qué release apareció un error. |
51
+ | `sampleRate` | `1.0` | Fracción de errores enviados. |
52
+ | `captureInteractions` | `true` | Registra clics, campos tocados y páginas visitadas del último minuto. |
53
+ | `queueCapacity` | `100` | Eventos en espera; al llenarse se descartan sin afectar a la app. |
54
+
55
+ ## Privacidad
56
+
57
+ - Nunca se envía el valor de un campo: solo qué campo se tocó.
58
+ - De los clics se envía el control (`button#guardar`) y la etiqueta visible solo de botones, enlaces, pestañas y
59
+ elementos de menú; nunca el contenido de tablas, textos o formularios.
60
+ - Para excluir una zona completa: `class="block-replay"` o el atributo `data-block-replay`.
61
+ - De la navegación se envía la ruta, sin parámetros de consulta.
62
+ - El SDK nunca lanza errores ni bloquea la interfaz: si IndeFlow no responde, el evento se pierde en silencio.
63
+
64
+ ## Trazas HTTP
65
+
66
+ `handleHttpRequestTrace` y `handleHttpResponseError` agregan la cabecera W3C `traceparent` y reportan respuestas 5xx.
67
+ Úsalos desde un interceptor HTTP si quieres correlacionar el error del navegador con el de tu API.
@@ -0,0 +1,28 @@
1
+ import type { IndeFlowAngularOptions, IndeFlowBrowserEvent } from './types';
2
+ import type { IndeFlowSessionReplay } from './replay';
3
+ export declare const DEFAULT_INGEST_ENDPOINT = "https://api.indeflow.app/api/v1/ingest/events";
4
+ export declare class IndeFlowBrowserClient {
5
+ private options;
6
+ private breadcrumbs;
7
+ private queue;
8
+ private isFlushing;
9
+ private droppedEvents;
10
+ private enqueuedEvents;
11
+ private interactionTrail;
12
+ constructor(options: IndeFlowAngularOptions);
13
+ /** Update settings that arrive after startup (for example a token fetched at runtime). Flushes anything queued. */
14
+ configure(update: Partial<IndeFlowAngularOptions>): void;
15
+ get isConfigured(): boolean;
16
+ /** Clicks, field focus and navigation recorded by the trail travel with every captured error. */
17
+ attachInteractionTrail(trail: IndeFlowSessionReplay | null): void;
18
+ private collectBreadcrumbs;
19
+ get droppedCount(): number;
20
+ get enqueuedCount(): number;
21
+ addBreadcrumb(category: string, message: string, level?: 'info' | 'warning' | 'error', data?: Record<string, string>): void;
22
+ captureException(error: unknown, additionalContext?: Partial<IndeFlowBrowserEvent>): void;
23
+ private resolveCulprit;
24
+ private detectBrowser;
25
+ private detectOs;
26
+ private scheduleFlush;
27
+ private flushQueue;
28
+ }
@@ -0,0 +1,10 @@
1
+ import { IndeFlowBrowserClient } from './client';
2
+ export interface ErrorHandlerInterface {
3
+ handleError(error: unknown): void;
4
+ }
5
+ export declare class IndeFlowErrorHandler implements ErrorHandlerInterface {
6
+ private client;
7
+ constructor(client: IndeFlowBrowserClient);
8
+ handleError(error: unknown): void;
9
+ }
10
+ export declare function initGlobalErrorListeners(client: IndeFlowBrowserClient): () => void;
@@ -0,0 +1,13 @@
1
+ export * from './types';
2
+ export * from './client';
3
+ export * from './error-handler';
4
+ export * from './interceptor';
5
+ export * from './replay';
6
+ import { IndeFlowBrowserClient } from './client';
7
+ import type { IndeFlowAngularOptions } from './types';
8
+ export declare const SDK_VERSION = "1.1.1";
9
+ /**
10
+ * Creates the client, listens for uncaught errors and (unless captureInteractions is false)
11
+ * records the trail of clicks and navigation that is attached to each error.
12
+ */
13
+ export declare function createIndeFlowObservabilityClient(options: IndeFlowAngularOptions): IndeFlowBrowserClient;
package/dist/index.js ADDED
@@ -0,0 +1,449 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+
5
+ // packages/angular/observability-angular/src/client.ts
6
+ var DEFAULT_INGEST_ENDPOINT = "https://api.indeflow.app/api/v1/ingest/events";
7
+ var IndeFlowBrowserClient = class {
8
+ constructor(options) {
9
+ __publicField(this, "options");
10
+ __publicField(this, "breadcrumbs", []);
11
+ __publicField(this, "queue", []);
12
+ __publicField(this, "isFlushing", false);
13
+ __publicField(this, "droppedEvents", 0);
14
+ __publicField(this, "enqueuedEvents", 0);
15
+ __publicField(this, "interactionTrail", null);
16
+ this.options = {
17
+ ingestEndpoint: options.ingestEndpoint || DEFAULT_INGEST_ENDPOINT,
18
+ sourceToken: options.sourceToken || "",
19
+ environment: options.environment || "production",
20
+ release: options.release || "1.0.0",
21
+ sampleRate: options.sampleRate ?? 1,
22
+ maxBreadcrumbs: options.maxBreadcrumbs ?? 25,
23
+ queueCapacity: options.queueCapacity ?? 100,
24
+ maskAllInputs: options.maskAllInputs ?? true,
25
+ enableHttpTracking: options.enableHttpTracking ?? true,
26
+ enableRoutingTracking: options.enableRoutingTracking ?? true,
27
+ captureInteractions: options.captureInteractions ?? true
28
+ };
29
+ }
30
+ /** Update settings that arrive after startup (for example a token fetched at runtime). Flushes anything queued. */
31
+ configure(update) {
32
+ for (const [key, value] of Object.entries(update)) {
33
+ if (value !== void 0 && value !== null && value !== "") {
34
+ this.options[key] = value;
35
+ }
36
+ }
37
+ if (this.queue.length > 0) this.scheduleFlush();
38
+ }
39
+ get isConfigured() {
40
+ return !!this.options.sourceToken;
41
+ }
42
+ /** Clicks, field focus and navigation recorded by the trail travel with every captured error. */
43
+ attachInteractionTrail(trail) {
44
+ this.interactionTrail = trail;
45
+ }
46
+ collectBreadcrumbs() {
47
+ const merged = [...this.breadcrumbs];
48
+ if (this.interactionTrail) {
49
+ for (const step of this.interactionTrail.getTrail(60)) {
50
+ merged.push({ timestampUtc: new Date(step.timestamp).toISOString(), category: step.category, message: step.message, level: "info" });
51
+ }
52
+ }
53
+ merged.sort((a, b) => a.timestampUtc.localeCompare(b.timestampUtc));
54
+ return merged.slice(-40);
55
+ }
56
+ get droppedCount() {
57
+ return this.droppedEvents;
58
+ }
59
+ get enqueuedCount() {
60
+ return this.enqueuedEvents;
61
+ }
62
+ addBreadcrumb(category, message, level = "info", data) {
63
+ if (this.breadcrumbs.length >= this.options.maxBreadcrumbs) {
64
+ this.breadcrumbs.shift();
65
+ }
66
+ this.breadcrumbs.push({
67
+ timestampUtc: (/* @__PURE__ */ new Date()).toISOString(),
68
+ category,
69
+ message,
70
+ level,
71
+ data
72
+ });
73
+ }
74
+ captureException(error, additionalContext) {
75
+ if (Math.random() > this.options.sampleRate) return;
76
+ try {
77
+ const err = error instanceof Error ? error : new Error(String(error));
78
+ const eventId = `evt_js_${Math.random().toString(36).substring(2, 10)}${Date.now().toString(36)}`;
79
+ const stack = err.stack || "";
80
+ const culprit = this.resolveCulprit(stack) || err.name;
81
+ const event = {
82
+ eventId,
83
+ timestampUtc: (/* @__PURE__ */ new Date()).toISOString(),
84
+ platform: "angular",
85
+ runtime: "V8 / Browser",
86
+ level: "error",
87
+ exceptionType: err.name || "Error",
88
+ culprit,
89
+ message: err.message || "Unknown javascript error",
90
+ stackTrace: stack,
91
+ tags: {
92
+ runtime: "browser",
93
+ angular: "true"
94
+ },
95
+ breadcrumbs: this.collectBreadcrumbs(),
96
+ environment: this.options.environment,
97
+ release: this.options.release,
98
+ url: typeof window !== "undefined" ? window.location?.href || "/" : "/",
99
+ deviceBrowser: this.detectBrowser(),
100
+ deviceOs: this.detectOs(),
101
+ viewport: typeof window !== "undefined" ? `${window.innerWidth}x${window.innerHeight}` : void 0,
102
+ ...additionalContext
103
+ };
104
+ if (this.queue.length >= this.options.queueCapacity) {
105
+ this.droppedEvents++;
106
+ return;
107
+ }
108
+ this.queue.push(event);
109
+ this.enqueuedEvents++;
110
+ this.scheduleFlush();
111
+ } catch {
112
+ }
113
+ }
114
+ resolveCulprit(stack) {
115
+ if (!stack) return "unknown";
116
+ const lines = stack.split("\n");
117
+ for (let i = 1; i < lines.length; i++) {
118
+ const line = lines[i].trim();
119
+ if (line && !line.includes("observability-angular") && !line.includes("zone.js")) {
120
+ return line.replace(/^at\s+/, "").trim();
121
+ }
122
+ }
123
+ return lines[1] ? lines[1].trim() : "window.onerror";
124
+ }
125
+ detectBrowser() {
126
+ if (typeof navigator === "undefined") return "Node / SSR";
127
+ const ua = navigator.userAgent;
128
+ const version = (pattern) => {
129
+ const m = ua.match(pattern);
130
+ return m ? ` ${m[1]}` : "";
131
+ };
132
+ if (ua.includes("Edg/")) return `Edge${version(/Edg\/(\d+)/)}`;
133
+ if (ua.includes("OPR/")) return `Opera${version(/OPR\/(\d+)/)}`;
134
+ if (ua.includes("Firefox/")) return `Firefox${version(/Firefox\/(\d+)/)}`;
135
+ if (ua.includes("Chrome/")) return `Chrome${version(/Chrome\/(\d+)/)}`;
136
+ if (ua.includes("Safari/")) return `Safari${version(/Version\/(\d+)/)}`;
137
+ return "Browser";
138
+ }
139
+ detectOs() {
140
+ if (typeof navigator === "undefined") return "Server";
141
+ const ua = navigator.userAgent;
142
+ const mobile = /Mobi|Android|iPhone|iPad/.test(ua) ? " (mobile)" : "";
143
+ if (ua.includes("Android")) return `Android${mobile}`;
144
+ if (ua.includes("iPhone") || ua.includes("iPad")) return `iOS${mobile}`;
145
+ if (ua.includes("Windows")) return "Windows";
146
+ if (ua.includes("Mac")) return "macOS";
147
+ if (ua.includes("Linux")) return "Linux";
148
+ return "Unknown OS";
149
+ }
150
+ scheduleFlush() {
151
+ if (this.isFlushing) return;
152
+ this.isFlushing = true;
153
+ const defer = typeof queueMicrotask === "function" ? queueMicrotask : (fn) => setTimeout(fn, 0);
154
+ defer(() => {
155
+ this.flushQueue().finally(() => {
156
+ this.isFlushing = false;
157
+ });
158
+ });
159
+ }
160
+ async flushQueue() {
161
+ if (this.queue.length === 0 || !this.options.sourceToken) return;
162
+ const eventsToSend = this.queue.splice(0, 25);
163
+ const body = JSON.stringify(eventsToSend.length === 1 ? eventsToSend[0] : eventsToSend);
164
+ try {
165
+ if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && eventsToSend.length === 1) {
166
+ const blob = new Blob([body], { type: "application/json" });
167
+ }
168
+ if (typeof fetch === "function") {
169
+ await fetch(this.options.ingestEndpoint, {
170
+ method: "POST",
171
+ headers: {
172
+ "Content-Type": "application/json",
173
+ "X-IndeFlow-Source-Token": this.options.sourceToken
174
+ },
175
+ body,
176
+ keepalive: true
177
+ });
178
+ }
179
+ } catch {
180
+ }
181
+ }
182
+ };
183
+
184
+ // packages/angular/observability-angular/src/error-handler.ts
185
+ var IndeFlowErrorHandler = class {
186
+ constructor(client) {
187
+ __publicField(this, "client");
188
+ this.client = client;
189
+ }
190
+ handleError(error) {
191
+ try {
192
+ this.client.captureException(error);
193
+ } catch {
194
+ }
195
+ if (typeof console !== "undefined" && typeof console.error === "function") {
196
+ console.error(error);
197
+ }
198
+ }
199
+ };
200
+ function initGlobalErrorListeners(client) {
201
+ if (typeof window === "undefined") {
202
+ return () => {
203
+ };
204
+ }
205
+ const isHttpResponse = (value) => typeof value === "object" && value !== null && !(value instanceof Error) && typeof value.status === "number" && "url" in value;
206
+ const isResizeObserverNotice = (message) => typeof message === "string" && message.includes("ResizeObserver loop");
207
+ const onErrorHandler = (event) => {
208
+ if (isHttpResponse(event.error) || isResizeObserverNotice(event.message)) return;
209
+ client.captureException(event.error || event.message, {
210
+ culprit: event.filename ? `${event.filename}:${event.lineno}:${event.colno}` : "window.onerror",
211
+ message: event.message || "Uncaught error"
212
+ });
213
+ };
214
+ const onUnhandledRejection = (event) => {
215
+ if (isHttpResponse(event.reason)) return;
216
+ client.captureException(event.reason, {
217
+ culprit: "UnhandledPromiseRejection",
218
+ message: event.reason instanceof Error ? event.reason.message : String(event.reason)
219
+ });
220
+ };
221
+ window.addEventListener("error", onErrorHandler);
222
+ window.addEventListener("unhandledrejection", onUnhandledRejection);
223
+ return () => {
224
+ window.removeEventListener("error", onErrorHandler);
225
+ window.removeEventListener("unhandledrejection", onUnhandledRejection);
226
+ };
227
+ }
228
+
229
+ // packages/angular/observability-angular/src/interceptor.ts
230
+ function generateW3CTraceparent() {
231
+ const bytes = new Uint8Array(24);
232
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
233
+ crypto.getRandomValues(bytes);
234
+ } else {
235
+ for (let i = 0; i < 24; i++) {
236
+ bytes[i] = Math.floor(Math.random() * 256);
237
+ }
238
+ }
239
+ const toHex = (buf) => Array.from(buf).map((b) => b.toString(16).padStart(2, "0")).join("");
240
+ const traceId = toHex(bytes.slice(0, 16));
241
+ const spanId = toHex(bytes.slice(16, 24));
242
+ const traceparent = `00-${traceId}-${spanId}-01`;
243
+ return { traceparent, traceId, spanId };
244
+ }
245
+ function handleHttpRequestTrace(client, req) {
246
+ const { traceparent, traceId, spanId } = generateW3CTraceparent();
247
+ const headers = req.headers.set("traceparent", traceparent);
248
+ const cloned = req.clone({ headers });
249
+ client.addBreadcrumb("http", `${req.method.toUpperCase()} ${req.url}`, "info", {
250
+ traceId,
251
+ spanId
252
+ });
253
+ return { req: cloned, traceId, spanId };
254
+ }
255
+ function handleHttpResponseError(client, req, status, statusText, errorBody, traceId, spanId) {
256
+ const isServerError = status >= 500 || status === 0;
257
+ const level = isServerError ? "error" : "warning";
258
+ client.addBreadcrumb("http", `${req.method.toUpperCase()} ${req.url} [${status} ${statusText}]`, level, {
259
+ status: String(status),
260
+ statusText,
261
+ traceId: traceId || ""
262
+ });
263
+ if (isServerError) {
264
+ const errorMsg = `HTTP ${req.method.toUpperCase()} ${req.url} failed with status ${status}: ${statusText}`;
265
+ client.captureException(new Error(errorMsg), {
266
+ culprit: `${req.method.toUpperCase()} ${req.url}`,
267
+ message: errorMsg,
268
+ level: "error",
269
+ httpMethod: req.method,
270
+ httpStatusCode: status,
271
+ traceId,
272
+ spanId
273
+ });
274
+ }
275
+ }
276
+
277
+ // packages/angular/observability-angular/src/replay.ts
278
+ var CONTROL_SELECTOR = 'button, a, [role="button"], [role="tab"], [role="menuitem"], [role="link"], [role="option"], summary, label, input[type="submit"], input[type="button"]';
279
+ var IndeFlowSessionReplay = class {
280
+ constructor(options = {}) {
281
+ __publicField(this, "buffer", []);
282
+ __publicField(this, "sessionId");
283
+ __publicField(this, "maxDurationMs");
284
+ __publicField(this, "maskAllInputs");
285
+ __publicField(this, "blockSelector");
286
+ __publicField(this, "captureControlLabels");
287
+ __publicField(this, "isRecording", false);
288
+ __publicField(this, "cleanupListeners", []);
289
+ this.sessionId = `rep_${Math.random().toString(36).substring(2, 10)}_${Date.now().toString(36)}`;
290
+ this.maxDurationMs = (options.maxBufferDurationSeconds ?? 60) * 1e3;
291
+ this.maskAllInputs = options.maskAllInputs ?? true;
292
+ this.blockSelector = options.blockSelector ?? ".if-block, [data-if-block], .block-replay, [data-block-replay]";
293
+ this.captureControlLabels = options.captureControlLabels ?? true;
294
+ }
295
+ get currentSessionId() {
296
+ return this.sessionId;
297
+ }
298
+ start() {
299
+ if (this.isRecording || typeof window === "undefined") return;
300
+ this.isRecording = true;
301
+ this.recordEvent({
302
+ timestamp: Date.now(),
303
+ type: "snapshot",
304
+ data: {
305
+ url: window.location?.href || "/",
306
+ viewport: `${window.innerWidth}x${window.innerHeight}`,
307
+ title: document.title
308
+ }
309
+ });
310
+ const clickHandler = (e) => {
311
+ const target = e.target;
312
+ if (!target || this.isBlocked(target)) return;
313
+ const control = target.closest ? target.closest(CONTROL_SELECTOR) : null;
314
+ const el = control ?? target;
315
+ this.recordEvent({
316
+ timestamp: Date.now(),
317
+ type: "click",
318
+ target: this.describe(el),
319
+ data: { text: control && this.captureControlLabels ? this.controlLabel(control) : "" }
320
+ });
321
+ };
322
+ const inputHandler = (e) => {
323
+ const target = e.target;
324
+ if (!target) return;
325
+ const type = (target.getAttribute("type") || "").toLowerCase();
326
+ const isPassword = type === "password";
327
+ const isSensitiveName = /card|cvc|cvv|pass|token|secret|pin/i.test(target.name || target.id || "");
328
+ const value = isPassword || isSensitiveName || this.maskAllInputs ? "***" : target.value ? target.value.substring(0, 50) : "";
329
+ const inputTarget = `${target.tagName.toLowerCase()}${target.id ? "#" + target.id : target.name ? `[name=${target.name}]` : ""}`;
330
+ const last = this.buffer[this.buffer.length - 1];
331
+ if (last && last.type === "input" && last.target === inputTarget) {
332
+ last.timestamp = Date.now();
333
+ return;
334
+ }
335
+ this.recordEvent({
336
+ timestamp: Date.now(),
337
+ type: "input",
338
+ target: inputTarget,
339
+ data: {
340
+ inputType: type,
341
+ maskedValue: value
342
+ }
343
+ });
344
+ };
345
+ let lastPath = window.location?.pathname || "/";
346
+ const navigationHandler = () => {
347
+ const path = window.location?.pathname || "/";
348
+ if (path === lastPath) return;
349
+ lastPath = path;
350
+ this.recordEvent({ timestamp: Date.now(), type: "navigation", data: { to: path } });
351
+ };
352
+ const navigationPoll = setInterval(navigationHandler, 1e3);
353
+ window.addEventListener("click", clickHandler, { capture: true, passive: true });
354
+ window.addEventListener("input", inputHandler, { capture: true, passive: true });
355
+ window.addEventListener("popstate", navigationHandler);
356
+ this.cleanupListeners.push(() => {
357
+ clearInterval(navigationPoll);
358
+ window.removeEventListener("click", clickHandler, { capture: true });
359
+ window.removeEventListener("input", inputHandler, { capture: true });
360
+ window.removeEventListener("popstate", navigationHandler);
361
+ });
362
+ }
363
+ describe(el) {
364
+ const tag = el.tagName.toLowerCase();
365
+ const id = el.id ? `#${el.id}` : "";
366
+ const testId = el.getAttribute("data-testid") || el.getAttribute("data-test");
367
+ const role = el.getAttribute("role");
368
+ const name = !id && el.getAttribute("name") ? `[name=${el.getAttribute("name")}]` : "";
369
+ return `${tag}${id}${name}${role ? `[role=${role}]` : ""}${testId ? `[testid=${testId}]` : ""}`.substring(0, 120);
370
+ }
371
+ controlLabel(el) {
372
+ const raw = el.getAttribute("aria-label") || el.getAttribute("title") || el.value || el.innerText || "";
373
+ return raw.replace(/\s+/g, " ").trim().substring(0, 40);
374
+ }
375
+ stop() {
376
+ this.isRecording = false;
377
+ for (const cleanup of this.cleanupListeners) {
378
+ cleanup();
379
+ }
380
+ this.cleanupListeners = [];
381
+ }
382
+ recordEvent(event) {
383
+ const now = Date.now();
384
+ this.buffer.push(event);
385
+ const cutoff = now - this.maxDurationMs;
386
+ while (this.buffer.length > 0 && this.buffer[0].timestamp < cutoff) {
387
+ this.buffer.shift();
388
+ }
389
+ }
390
+ getRecentEvents(secondsBeforeNow = 30) {
391
+ const cutoff = Date.now() - secondsBeforeNow * 1e3;
392
+ return this.buffer.filter((e) => e.timestamp >= cutoff);
393
+ }
394
+ getAllEvents() {
395
+ return [...this.buffer];
396
+ }
397
+ /** The recent trail as readable steps, ready to travel inside the error event. */
398
+ getTrail(secondsBeforeNow = 60) {
399
+ return this.getRecentEvents(secondsBeforeNow).map((e) => {
400
+ if (e.type === "click") {
401
+ const label = e.data && typeof e.data["text"] === "string" && e.data["text"] ? ` "${e.data["text"]}"` : "";
402
+ return { timestamp: e.timestamp, category: "ui.click", message: `${e.target || "element"}${label}` };
403
+ }
404
+ if (e.type === "input") {
405
+ return { timestamp: e.timestamp, category: "ui.input", message: `${e.target || "field"} (value hidden)` };
406
+ }
407
+ if (e.type === "navigation") {
408
+ return { timestamp: e.timestamp, category: "navigation", message: String(e.data && e.data["to"] || "/") };
409
+ }
410
+ if (e.type === "snapshot") {
411
+ const url = String(e.data && e.data["url"] || "/").split("?")[0];
412
+ return { timestamp: e.timestamp, category: "page", message: `${url} ${String(e.data && e.data["viewport"] || "")}`.trim() };
413
+ }
414
+ return { timestamp: e.timestamp, category: e.type, message: e.target || e.type };
415
+ });
416
+ }
417
+ isBlocked(el) {
418
+ if (el.matches && el.matches(this.blockSelector)) return true;
419
+ if (el.closest && el.closest(this.blockSelector)) return true;
420
+ return false;
421
+ }
422
+ };
423
+
424
+ // packages/angular/observability-angular/src/index.ts
425
+ var SDK_VERSION = "1.1.1";
426
+ function createIndeFlowObservabilityClient(options) {
427
+ const client = new IndeFlowBrowserClient(options);
428
+ if (typeof window !== "undefined") {
429
+ initGlobalErrorListeners(client);
430
+ if (options.captureInteractions !== false) {
431
+ const trail = new IndeFlowSessionReplay({ maskAllInputs: options.maskAllInputs ?? true });
432
+ trail.start();
433
+ client.attachInteractionTrail(trail);
434
+ }
435
+ }
436
+ return client;
437
+ }
438
+ export {
439
+ DEFAULT_INGEST_ENDPOINT,
440
+ IndeFlowBrowserClient,
441
+ IndeFlowErrorHandler,
442
+ IndeFlowSessionReplay,
443
+ SDK_VERSION,
444
+ createIndeFlowObservabilityClient,
445
+ generateW3CTraceparent,
446
+ handleHttpRequestTrace,
447
+ handleHttpResponseError,
448
+ initGlobalErrorListeners
449
+ };
@@ -0,0 +1,26 @@
1
+ import { IndeFlowBrowserClient } from './client';
2
+ export declare function generateW3CTraceparent(): {
3
+ traceparent: string;
4
+ traceId: string;
5
+ spanId: string;
6
+ };
7
+ export interface HttpRequestLike {
8
+ url: string;
9
+ method: string;
10
+ headers: {
11
+ set(name: string, value: string): HttpRequestLike['headers'];
12
+ get?(name: string): string | null;
13
+ };
14
+ clone(update: {
15
+ headers: HttpRequestLike['headers'];
16
+ }): HttpRequestLike;
17
+ }
18
+ export declare function handleHttpRequestTrace(client: IndeFlowBrowserClient, req: HttpRequestLike): {
19
+ req: HttpRequestLike;
20
+ traceId: string;
21
+ spanId: string;
22
+ };
23
+ export declare function handleHttpResponseError(client: IndeFlowBrowserClient, req: {
24
+ method: string;
25
+ url: string;
26
+ }, status: number, statusText: string, errorBody?: unknown, traceId?: string, spanId?: string): void;
@@ -0,0 +1,40 @@
1
+ export interface ReplayOptions {
2
+ maskAllInputs?: boolean;
3
+ blockSelector?: string;
4
+ maxBufferDurationSeconds?: number;
5
+ sampleRate?: number;
6
+ /** Capture the visible label of buttons, links, tabs and menu items (never of inputs or free content). Default true. */
7
+ captureControlLabels?: boolean;
8
+ }
9
+ export interface ReplayEvent {
10
+ timestamp: number;
11
+ type: 'snapshot' | 'mutation' | 'click' | 'input' | 'navigation' | 'error';
12
+ target?: string;
13
+ data?: Record<string, unknown>;
14
+ }
15
+ export declare class IndeFlowSessionReplay {
16
+ private buffer;
17
+ private sessionId;
18
+ private maxDurationMs;
19
+ private maskAllInputs;
20
+ private blockSelector;
21
+ private captureControlLabels;
22
+ private isRecording;
23
+ private cleanupListeners;
24
+ constructor(options?: ReplayOptions);
25
+ get currentSessionId(): string;
26
+ start(): void;
27
+ private describe;
28
+ private controlLabel;
29
+ stop(): void;
30
+ recordEvent(event: ReplayEvent): void;
31
+ getRecentEvents(secondsBeforeNow?: number): ReplayEvent[];
32
+ getAllEvents(): ReplayEvent[];
33
+ /** The recent trail as readable steps, ready to travel inside the error event. */
34
+ getTrail(secondsBeforeNow?: number): Array<{
35
+ timestamp: number;
36
+ category: string;
37
+ message: string;
38
+ }>;
39
+ private isBlocked;
40
+ }
@@ -0,0 +1,47 @@
1
+ export interface IndeFlowAngularOptions {
2
+ /** Defaults to https://api.indeflow.app/api/v1/ingest/events */
3
+ ingestEndpoint?: string;
4
+ sourceToken: string;
5
+ environment?: string;
6
+ release?: string;
7
+ sampleRate?: number;
8
+ maxBreadcrumbs?: number;
9
+ queueCapacity?: number;
10
+ maskAllInputs?: boolean;
11
+ enableHttpTracking?: boolean;
12
+ enableRoutingTracking?: boolean;
13
+ /** Record the sequence of clicks, fields touched and pages visited before an error. Default true. */
14
+ captureInteractions?: boolean;
15
+ }
16
+ export interface IndeFlowBreadcrumb {
17
+ timestampUtc: string;
18
+ category: string;
19
+ message: string;
20
+ level?: 'info' | 'warning' | 'error';
21
+ data?: Record<string, string>;
22
+ }
23
+ export interface IndeFlowBrowserEvent {
24
+ eventId: string;
25
+ timestampUtc: string;
26
+ platform: string;
27
+ runtime: string;
28
+ level: 'error' | 'warning' | 'info';
29
+ exceptionType: string;
30
+ culprit: string;
31
+ message: string;
32
+ stackTrace?: string;
33
+ tags: Record<string, string>;
34
+ breadcrumbs: IndeFlowBreadcrumb[];
35
+ traceId?: string;
36
+ spanId?: string;
37
+ environment: string;
38
+ release?: string;
39
+ url: string;
40
+ deviceBrowser?: string;
41
+ deviceOs?: string;
42
+ viewport?: string;
43
+ userIdentifierMasked?: string;
44
+ httpMethod?: string;
45
+ httpStatusCode?: number;
46
+ durationMs?: number;
47
+ }
package/package.json CHANGED
@@ -1,6 +1,33 @@
1
1
  {
2
2
  "name": "@indesad/observability-angular",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.1.1",
4
+ "description": "Official Angular & Browser Observability SDK for IndeFlow with fail-open error handling, W3C trace propagation and private source maps support.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "sideEffects": false,
21
+ "author": "INDESAD",
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/INDESAD/IndeFlow.git",
26
+ "directory": "packages/angular/observability-angular"
27
+ },
28
+ "homepage": "https://indeflow.app",
29
+ "publishConfig": {
30
+ "access": "public",
31
+ "registry": "https://registry.npmjs.org/"
32
+ }
33
+ }