@magicfeedback/popup-sdk 1.1.2 → 1.1.4
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/dist/index.d.mts +116 -3
- package/dist/index.d.ts +116 -3
- package/dist/index.js +232 -31
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +232 -31
- package/dist/index.mjs.map +1 -1
- package/dist/react-native.d.mts +58 -2
- package/dist/react-native.d.ts +58 -2
- package/dist/react-native.js +229 -31
- package/dist/react-native.js.map +1 -1
- package/dist/react-native.mjs +229 -31
- package/dist/react-native.mjs.map +1 -1
- package/package.json +1 -1
package/dist/react-native.d.mts
CHANGED
|
@@ -216,6 +216,52 @@ interface PopupRenderer {
|
|
|
216
216
|
type ContactAttributeValue = string | number | boolean;
|
|
217
217
|
type ContactAttributes = Record<string, ContactAttributeValue>;
|
|
218
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Crash & error reporting (Stability #14–17) — captura de errores GESTIONADOS.
|
|
221
|
+
*
|
|
222
|
+
* - `reportError()`: API pública para el host (app viva) → emite `deepdots_app_crash` ya.
|
|
223
|
+
* - handlers de `window` (errores no capturados): persisten a disco y se reenvían
|
|
224
|
+
* en el siguiente arranque (el proceso puede morir antes del flush normal).
|
|
225
|
+
*
|
|
226
|
+
* El contexto de device/sesión se captura EN EL MOMENTO del crash y se guarda en el
|
|
227
|
+
* record — no se lee del envelope en el replay (el replay puede ocurrir en otra
|
|
228
|
+
* app_version/OS). Las dims OS/device en Web suelen ir vacías (se derivan del UA en backend).
|
|
229
|
+
*/
|
|
230
|
+
|
|
231
|
+
type CrashSeverity = 'fatal' | 'error' | 'warning';
|
|
232
|
+
interface ReportErrorOptions {
|
|
233
|
+
severity?: CrashSeverity;
|
|
234
|
+
handled?: boolean;
|
|
235
|
+
context?: Record<string, unknown>;
|
|
236
|
+
}
|
|
237
|
+
/** Forma mínima de `global.ErrorUtils` de React Native. */
|
|
238
|
+
interface ReactNativeErrorUtils {
|
|
239
|
+
getGlobalHandler?(): ((error: unknown, isFatal?: boolean) => void) | undefined;
|
|
240
|
+
setGlobalHandler(handler: (error: unknown, isFatal?: boolean) => void): void;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Messaging (#18–22): tracking de notificaciones del host (push / in-app).
|
|
245
|
+
* Un único evento reservado `deepdots_message` con un campo `stage` discriminador;
|
|
246
|
+
* el funnel se correlaciona por `message_id` y se agrupa por `message_title`.
|
|
247
|
+
*/
|
|
248
|
+
type MessageStage = 'delivered' | 'clicked' | 'converted';
|
|
249
|
+
interface TrackMessageOptions {
|
|
250
|
+
/** Identificador del mensaje/notificación — correlaciona el funnel delivered→clicked→converted. */
|
|
251
|
+
id: string;
|
|
252
|
+
/** Título del mensaje — dimensión de agrupación de #18–22. */
|
|
253
|
+
title: string;
|
|
254
|
+
/** Canal de entrega. */
|
|
255
|
+
channel: 'push' | 'in_app';
|
|
256
|
+
/** Nombre de la campaña (opcional). */
|
|
257
|
+
campaign?: string;
|
|
258
|
+
/** Valor de conversión (opcional, típico en `stage: 'converted'`). */
|
|
259
|
+
value?: number;
|
|
260
|
+
currency?: string;
|
|
261
|
+
/** Parámetros libres adicionales. */
|
|
262
|
+
params?: Record<string, unknown>;
|
|
263
|
+
}
|
|
264
|
+
|
|
219
265
|
/**
|
|
220
266
|
* Main class for managing survey popups
|
|
221
267
|
*/
|
|
@@ -244,6 +290,8 @@ declare class DeepdotsPopups {
|
|
|
244
290
|
private analyticsFeedbackSessionId;
|
|
245
291
|
/** Timer del flush periódico de analytics (cada ANALYTICS_FLUSH_INTERVAL_MS). */
|
|
246
292
|
private analyticsFlushTimer;
|
|
293
|
+
/** Crash & error reporting (#14–17). Null hasta init(). */
|
|
294
|
+
private crashReporter;
|
|
247
295
|
/** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
|
|
248
296
|
private navObserver;
|
|
249
297
|
/** Tiempo activo (engagement time, #8). */
|
|
@@ -264,6 +312,10 @@ declare class DeepdotsPopups {
|
|
|
264
312
|
track(name: string, params?: Record<string, unknown>): void;
|
|
265
313
|
/** User attributes para breakdowns (registration_status, pass_type, sector, pass_status…). */
|
|
266
314
|
setUserAttributes(attributes: Record<string, string | number | boolean>): void;
|
|
315
|
+
/** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
|
|
316
|
+
reportError(error: unknown, options?: ReportErrorOptions): void;
|
|
317
|
+
/** Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. No-op si tracking off. */
|
|
318
|
+
installReactNativeCrashHandler(errorUtils: ReactNativeErrorUtils): void;
|
|
267
319
|
/**
|
|
268
320
|
* Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
|
|
269
321
|
* Contact del backend para segmentación/targeting de popups. Identificada por el `userId` del
|
|
@@ -273,14 +325,16 @@ declare class DeepdotsPopups {
|
|
|
273
325
|
setContactAttributes(attributes: ContactAttributes): Promise<boolean>;
|
|
274
326
|
/** Marca el inicio de un mini-service; etiqueta los eventos siguientes. No-op si tracking off. */
|
|
275
327
|
enterMiniService(name: string, entryPointType?: string): void;
|
|
276
|
-
/** Cierra el mini-service
|
|
277
|
-
exitMiniService(): void;
|
|
328
|
+
/** Cierra el mini-service `name` (emite `mini_service_exit` con duración, #27). No-op si tracking off o si ese no está activo. */
|
|
329
|
+
exitMiniService(name: string): void;
|
|
278
330
|
/** Findability (#31/#35): registra una búsqueda. `has_results` se deriva de `resultsCount`. */
|
|
279
331
|
trackSearch(query: string, resultsCount: number, params?: Record<string, unknown>): void;
|
|
280
332
|
/** Findability friction (#34/#35): señal de fricción con su `friction_topic`. */
|
|
281
333
|
trackFindabilityFriction(frictionTopic: string, params?: Record<string, unknown>): void;
|
|
282
334
|
/** Funnel: un paso del embudo, correlacionado por `taskId`. El backend reconstruye conversión/drop-off/tiempo. */
|
|
283
335
|
trackFunnelStep(funnel: string, step: string, taskId: string, params?: Record<string, unknown>): void;
|
|
336
|
+
/** Messaging (#18–22): registra una etapa del funnel de una notificación (push/in-app). No-op si tracking off. */
|
|
337
|
+
trackMessage(stage: MessageStage, options: TrackMessageOptions): void;
|
|
284
338
|
/**
|
|
285
339
|
* Navegación MANUAL (entornos sin History API, p. ej. React Native con React Navigation).
|
|
286
340
|
* El host la llama en cada cambio de pantalla; emite `page_view` al salir de la anterior.
|
|
@@ -406,6 +460,8 @@ interface ReactNativeSetupDeps {
|
|
|
406
460
|
appState?: AppStateLike | null;
|
|
407
461
|
platform?: 'web' | 'android' | 'ios';
|
|
408
462
|
renderer?: PopupRenderer;
|
|
463
|
+
/** `global.ErrorUtils` de RN (para capturar errores JS no manejados). Default: `globalThis.ErrorUtils`. */
|
|
464
|
+
errorUtils?: ReactNativeErrorUtils | null;
|
|
409
465
|
}
|
|
410
466
|
/**
|
|
411
467
|
* Configura el SDK para React Native en una sola llamada: storage persistente (MMKV),
|
package/dist/react-native.d.ts
CHANGED
|
@@ -216,6 +216,52 @@ interface PopupRenderer {
|
|
|
216
216
|
type ContactAttributeValue = string | number | boolean;
|
|
217
217
|
type ContactAttributes = Record<string, ContactAttributeValue>;
|
|
218
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Crash & error reporting (Stability #14–17) — captura de errores GESTIONADOS.
|
|
221
|
+
*
|
|
222
|
+
* - `reportError()`: API pública para el host (app viva) → emite `deepdots_app_crash` ya.
|
|
223
|
+
* - handlers de `window` (errores no capturados): persisten a disco y se reenvían
|
|
224
|
+
* en el siguiente arranque (el proceso puede morir antes del flush normal).
|
|
225
|
+
*
|
|
226
|
+
* El contexto de device/sesión se captura EN EL MOMENTO del crash y se guarda en el
|
|
227
|
+
* record — no se lee del envelope en el replay (el replay puede ocurrir en otra
|
|
228
|
+
* app_version/OS). Las dims OS/device en Web suelen ir vacías (se derivan del UA en backend).
|
|
229
|
+
*/
|
|
230
|
+
|
|
231
|
+
type CrashSeverity = 'fatal' | 'error' | 'warning';
|
|
232
|
+
interface ReportErrorOptions {
|
|
233
|
+
severity?: CrashSeverity;
|
|
234
|
+
handled?: boolean;
|
|
235
|
+
context?: Record<string, unknown>;
|
|
236
|
+
}
|
|
237
|
+
/** Forma mínima de `global.ErrorUtils` de React Native. */
|
|
238
|
+
interface ReactNativeErrorUtils {
|
|
239
|
+
getGlobalHandler?(): ((error: unknown, isFatal?: boolean) => void) | undefined;
|
|
240
|
+
setGlobalHandler(handler: (error: unknown, isFatal?: boolean) => void): void;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Messaging (#18–22): tracking de notificaciones del host (push / in-app).
|
|
245
|
+
* Un único evento reservado `deepdots_message` con un campo `stage` discriminador;
|
|
246
|
+
* el funnel se correlaciona por `message_id` y se agrupa por `message_title`.
|
|
247
|
+
*/
|
|
248
|
+
type MessageStage = 'delivered' | 'clicked' | 'converted';
|
|
249
|
+
interface TrackMessageOptions {
|
|
250
|
+
/** Identificador del mensaje/notificación — correlaciona el funnel delivered→clicked→converted. */
|
|
251
|
+
id: string;
|
|
252
|
+
/** Título del mensaje — dimensión de agrupación de #18–22. */
|
|
253
|
+
title: string;
|
|
254
|
+
/** Canal de entrega. */
|
|
255
|
+
channel: 'push' | 'in_app';
|
|
256
|
+
/** Nombre de la campaña (opcional). */
|
|
257
|
+
campaign?: string;
|
|
258
|
+
/** Valor de conversión (opcional, típico en `stage: 'converted'`). */
|
|
259
|
+
value?: number;
|
|
260
|
+
currency?: string;
|
|
261
|
+
/** Parámetros libres adicionales. */
|
|
262
|
+
params?: Record<string, unknown>;
|
|
263
|
+
}
|
|
264
|
+
|
|
219
265
|
/**
|
|
220
266
|
* Main class for managing survey popups
|
|
221
267
|
*/
|
|
@@ -244,6 +290,8 @@ declare class DeepdotsPopups {
|
|
|
244
290
|
private analyticsFeedbackSessionId;
|
|
245
291
|
/** Timer del flush periódico de analytics (cada ANALYTICS_FLUSH_INTERVAL_MS). */
|
|
246
292
|
private analyticsFlushTimer;
|
|
293
|
+
/** Crash & error reporting (#14–17). Null hasta init(). */
|
|
294
|
+
private crashReporter;
|
|
247
295
|
/** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
|
|
248
296
|
private navObserver;
|
|
249
297
|
/** Tiempo activo (engagement time, #8). */
|
|
@@ -264,6 +312,10 @@ declare class DeepdotsPopups {
|
|
|
264
312
|
track(name: string, params?: Record<string, unknown>): void;
|
|
265
313
|
/** User attributes para breakdowns (registration_status, pass_type, sector, pass_status…). */
|
|
266
314
|
setUserAttributes(attributes: Record<string, string | number | boolean>): void;
|
|
315
|
+
/** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
|
|
316
|
+
reportError(error: unknown, options?: ReportErrorOptions): void;
|
|
317
|
+
/** Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. No-op si tracking off. */
|
|
318
|
+
installReactNativeCrashHandler(errorUtils: ReactNativeErrorUtils): void;
|
|
267
319
|
/**
|
|
268
320
|
* Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
|
|
269
321
|
* Contact del backend para segmentación/targeting de popups. Identificada por el `userId` del
|
|
@@ -273,14 +325,16 @@ declare class DeepdotsPopups {
|
|
|
273
325
|
setContactAttributes(attributes: ContactAttributes): Promise<boolean>;
|
|
274
326
|
/** Marca el inicio de un mini-service; etiqueta los eventos siguientes. No-op si tracking off. */
|
|
275
327
|
enterMiniService(name: string, entryPointType?: string): void;
|
|
276
|
-
/** Cierra el mini-service
|
|
277
|
-
exitMiniService(): void;
|
|
328
|
+
/** Cierra el mini-service `name` (emite `mini_service_exit` con duración, #27). No-op si tracking off o si ese no está activo. */
|
|
329
|
+
exitMiniService(name: string): void;
|
|
278
330
|
/** Findability (#31/#35): registra una búsqueda. `has_results` se deriva de `resultsCount`. */
|
|
279
331
|
trackSearch(query: string, resultsCount: number, params?: Record<string, unknown>): void;
|
|
280
332
|
/** Findability friction (#34/#35): señal de fricción con su `friction_topic`. */
|
|
281
333
|
trackFindabilityFriction(frictionTopic: string, params?: Record<string, unknown>): void;
|
|
282
334
|
/** Funnel: un paso del embudo, correlacionado por `taskId`. El backend reconstruye conversión/drop-off/tiempo. */
|
|
283
335
|
trackFunnelStep(funnel: string, step: string, taskId: string, params?: Record<string, unknown>): void;
|
|
336
|
+
/** Messaging (#18–22): registra una etapa del funnel de una notificación (push/in-app). No-op si tracking off. */
|
|
337
|
+
trackMessage(stage: MessageStage, options: TrackMessageOptions): void;
|
|
284
338
|
/**
|
|
285
339
|
* Navegación MANUAL (entornos sin History API, p. ej. React Native con React Navigation).
|
|
286
340
|
* El host la llama en cada cambio de pantalla; emite `page_view` al salir de la anterior.
|
|
@@ -406,6 +460,8 @@ interface ReactNativeSetupDeps {
|
|
|
406
460
|
appState?: AppStateLike | null;
|
|
407
461
|
platform?: 'web' | 'android' | 'ios';
|
|
408
462
|
renderer?: PopupRenderer;
|
|
463
|
+
/** `global.ErrorUtils` de RN (para capturar errores JS no manejados). Default: `globalThis.ErrorUtils`. */
|
|
464
|
+
errorUtils?: ReactNativeErrorUtils | null;
|
|
409
465
|
}
|
|
410
466
|
/**
|
|
411
467
|
* Configura el SDK para React Native en una sola llamada: storage persistente (MMKV),
|
package/dist/react-native.js
CHANGED
|
@@ -1925,8 +1925,8 @@ var AnalyticsManager = class {
|
|
|
1925
1925
|
constructor(options = {}) {
|
|
1926
1926
|
this.events = [];
|
|
1927
1927
|
this.attributes = {};
|
|
1928
|
-
|
|
1929
|
-
this.
|
|
1928
|
+
/** Mini-services activos: nombre → timestamp de entrada. El orden de inserción marca el "más reciente". */
|
|
1929
|
+
this.activeMiniServices = /* @__PURE__ */ new Map();
|
|
1930
1930
|
this.sink = options.sink ?? dryRunSink;
|
|
1931
1931
|
this.now = options.now ?? (() => Date.now());
|
|
1932
1932
|
this.publicKey = options.publicKey;
|
|
@@ -1947,24 +1947,38 @@ var AnalyticsManager = class {
|
|
|
1947
1947
|
this.attributes[k] = String(v);
|
|
1948
1948
|
}
|
|
1949
1949
|
}
|
|
1950
|
-
/**
|
|
1950
|
+
/**
|
|
1951
|
+
* Marca el inicio de un mini-service; etiqueta los eventos siguientes con `mini_service`.
|
|
1952
|
+
* Admite varios activos a la vez (concurrencia); el "actual" para etiquetar es el más reciente.
|
|
1953
|
+
* Reentrar con un nombre ya activo refresca su orden y su tiempo de entrada.
|
|
1954
|
+
*/
|
|
1951
1955
|
enterMiniService(name, entryPointType) {
|
|
1952
|
-
this.
|
|
1953
|
-
this.
|
|
1954
|
-
this.track("
|
|
1955
|
-
}
|
|
1956
|
-
/**
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1956
|
+
this.activeMiniServices.delete(name);
|
|
1957
|
+
this.activeMiniServices.set(name, this.now());
|
|
1958
|
+
this.track("deepdots_mini_service_enter", { entry_point_type: entryPointType ?? null });
|
|
1959
|
+
}
|
|
1960
|
+
/**
|
|
1961
|
+
* Cierra el mini-service `name` emitiendo `mini_service_exit` con su duración (#27).
|
|
1962
|
+
* No-op si ese nombre no está activo. Cierre coherente con concurrencia.
|
|
1963
|
+
*/
|
|
1964
|
+
exitMiniService(name) {
|
|
1965
|
+
const enteredAt = this.activeMiniServices.get(name);
|
|
1966
|
+
if (enteredAt === void 0) return;
|
|
1967
|
+
const durationSeconds = Math.max(0, Math.round((this.now() - enteredAt) / 1e3));
|
|
1968
|
+
this.activeMiniServices.delete(name);
|
|
1969
|
+
this.track("deepdots_mini_service_exit", { mini_service: name, duration_seconds: durationSeconds });
|
|
1970
|
+
}
|
|
1971
|
+
/** Cierra TODOS los mini-services activos (orden LIFO). Para el cierre por lifecycle (background/cierre). */
|
|
1972
|
+
exitAllMiniServices() {
|
|
1973
|
+
for (const name of Array.from(this.activeMiniServices.keys()).reverse()) {
|
|
1974
|
+
this.exitMiniService(name);
|
|
1975
|
+
}
|
|
1963
1976
|
}
|
|
1964
1977
|
/** Registra un evento de analítica (modelo GA: nombre + params). */
|
|
1965
1978
|
track(name, params) {
|
|
1979
|
+
const current = this.getMiniService();
|
|
1966
1980
|
const merged = {
|
|
1967
|
-
...
|
|
1981
|
+
...current ? { mini_service: current } : {},
|
|
1968
1982
|
...params ?? {}
|
|
1969
1983
|
};
|
|
1970
1984
|
this.events.push({
|
|
@@ -1976,9 +1990,11 @@ var AnalyticsManager = class {
|
|
|
1976
1990
|
this.onFlushNeeded?.();
|
|
1977
1991
|
}
|
|
1978
1992
|
}
|
|
1979
|
-
/** Mini-service activo
|
|
1993
|
+
/** Mini-service actual (el más reciente aún activo) para etiquetar eventos + metadata del survey (#33). */
|
|
1980
1994
|
getMiniService() {
|
|
1981
|
-
|
|
1995
|
+
let last = null;
|
|
1996
|
+
for (const name of this.activeMiniServices.keys()) last = name;
|
|
1997
|
+
return last;
|
|
1982
1998
|
}
|
|
1983
1999
|
/** Nº de eventos pendientes de flush. */
|
|
1984
2000
|
pending() {
|
|
@@ -2318,11 +2334,153 @@ var ContactManager = class {
|
|
|
2318
2334
|
}
|
|
2319
2335
|
};
|
|
2320
2336
|
|
|
2337
|
+
// src/analytics/crash-reporter.ts
|
|
2338
|
+
var STACK_MAX = 8e3;
|
|
2339
|
+
var QUEUE_KEY = "deepdots.crash.queue";
|
|
2340
|
+
var MAX_QUEUED = 20;
|
|
2341
|
+
function crashRecordToParams(r) {
|
|
2342
|
+
const params = {
|
|
2343
|
+
crashed_at: r.crashedAt,
|
|
2344
|
+
crash_type: r.crashType,
|
|
2345
|
+
message: r.message,
|
|
2346
|
+
stack: r.stack,
|
|
2347
|
+
fatal: r.fatal,
|
|
2348
|
+
handled: r.handled,
|
|
2349
|
+
severity: r.severity
|
|
2350
|
+
};
|
|
2351
|
+
if (r.sessionId) params.crashed_session_id = r.sessionId;
|
|
2352
|
+
if (r.appVersion) params.crashed_app_version = r.appVersion;
|
|
2353
|
+
if (r.osVersion) params.crashed_os_version = r.osVersion;
|
|
2354
|
+
if (r.deviceModel) params.crashed_device_model = r.deviceModel;
|
|
2355
|
+
if (r.context) {
|
|
2356
|
+
for (const [k, v] of Object.entries(r.context)) params[`ctx_${k}`] = v;
|
|
2357
|
+
}
|
|
2358
|
+
return params;
|
|
2359
|
+
}
|
|
2360
|
+
var CrashReporter = class {
|
|
2361
|
+
constructor(options) {
|
|
2362
|
+
this.options = options;
|
|
2363
|
+
this.now = options.now ?? (() => Date.now());
|
|
2364
|
+
}
|
|
2365
|
+
isEnabled() {
|
|
2366
|
+
return this.options.enabled ? this.options.enabled() : true;
|
|
2367
|
+
}
|
|
2368
|
+
/** Construye un CrashRecord capturando contexto en el momento del crash. */
|
|
2369
|
+
buildRecord(error, severity, handled, fatal, context) {
|
|
2370
|
+
const err = error;
|
|
2371
|
+
const isErr = error instanceof Error;
|
|
2372
|
+
const dev = this.options.device();
|
|
2373
|
+
const ctx = context ? Object.fromEntries(Object.entries(context).map(([k, v]) => [k, String(v)])) : void 0;
|
|
2374
|
+
return {
|
|
2375
|
+
crashedAt: this.now(),
|
|
2376
|
+
crashType: isErr && err.name || "Error",
|
|
2377
|
+
message: isErr ? String(err.message ?? "") : String(error),
|
|
2378
|
+
stack: (isErr && typeof err.stack === "string" ? err.stack : "").slice(0, STACK_MAX),
|
|
2379
|
+
fatal,
|
|
2380
|
+
handled,
|
|
2381
|
+
severity,
|
|
2382
|
+
sessionId: this.options.sessionId(),
|
|
2383
|
+
appVersion: dev.appVersion,
|
|
2384
|
+
osVersion: dev.osVersion,
|
|
2385
|
+
deviceModel: dev.deviceModel,
|
|
2386
|
+
context: ctx
|
|
2387
|
+
};
|
|
2388
|
+
}
|
|
2389
|
+
/** API pública del host: reporta un error (app viva) → emite el evento ya. */
|
|
2390
|
+
reportError(error, options = {}) {
|
|
2391
|
+
if (!this.isEnabled()) return;
|
|
2392
|
+
const severity = options.severity ?? "error";
|
|
2393
|
+
const handled = options.handled ?? true;
|
|
2394
|
+
const fatal = severity === "fatal";
|
|
2395
|
+
const record = this.buildRecord(error, severity, handled, fatal, options.context);
|
|
2396
|
+
this.options.emit(crashRecordToParams(record));
|
|
2397
|
+
}
|
|
2398
|
+
/** Captura un error no manejado (fatal) → persiste a disco para replay en el siguiente arranque. */
|
|
2399
|
+
captureUnhandled(error) {
|
|
2400
|
+
this.persist(this.buildRecord(error, "fatal", false, true));
|
|
2401
|
+
}
|
|
2402
|
+
/** Persiste un crash no capturado a disco para reenviarlo en el siguiente arranque. */
|
|
2403
|
+
persist(record) {
|
|
2404
|
+
let queue = [];
|
|
2405
|
+
try {
|
|
2406
|
+
const raw = this.options.storage.getItem(QUEUE_KEY);
|
|
2407
|
+
if (raw) queue = JSON.parse(raw);
|
|
2408
|
+
if (!Array.isArray(queue)) queue = [];
|
|
2409
|
+
} catch {
|
|
2410
|
+
queue = [];
|
|
2411
|
+
}
|
|
2412
|
+
queue.push(record);
|
|
2413
|
+
if (queue.length > MAX_QUEUED) queue = queue.slice(queue.length - MAX_QUEUED);
|
|
2414
|
+
try {
|
|
2415
|
+
this.options.storage.setItem(QUEUE_KEY, JSON.stringify(queue));
|
|
2416
|
+
} catch {
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
/** Lee y vacía la cola de crashes pendientes (llamado en init para el replay). */
|
|
2420
|
+
drainPendingCrashes() {
|
|
2421
|
+
let queue = [];
|
|
2422
|
+
try {
|
|
2423
|
+
const raw = this.options.storage.getItem(QUEUE_KEY);
|
|
2424
|
+
if (raw) queue = JSON.parse(raw);
|
|
2425
|
+
if (!Array.isArray(queue)) queue = [];
|
|
2426
|
+
} catch {
|
|
2427
|
+
queue = [];
|
|
2428
|
+
}
|
|
2429
|
+
if (queue.length) {
|
|
2430
|
+
try {
|
|
2431
|
+
this.options.storage.removeItem(QUEUE_KEY);
|
|
2432
|
+
} catch {
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
return queue;
|
|
2436
|
+
}
|
|
2437
|
+
/** Instala los handlers globales de errores no capturados (no-op sin `window`). */
|
|
2438
|
+
install() {
|
|
2439
|
+
if (typeof window === "undefined") return;
|
|
2440
|
+
window.addEventListener("error", (e) => {
|
|
2441
|
+
if (!this.isEnabled()) return;
|
|
2442
|
+
this.captureUnhandled(e.error ?? e.message ?? "error");
|
|
2443
|
+
});
|
|
2444
|
+
window.addEventListener("unhandledrejection", (e) => {
|
|
2445
|
+
if (!this.isEnabled()) return;
|
|
2446
|
+
this.captureUnhandled(e.reason ?? "unhandledrejection");
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
/** Engancha `global.ErrorUtils` de RN (no hay `window`): persiste y encadena al handler previo. */
|
|
2450
|
+
installReactNative(errorUtils) {
|
|
2451
|
+
const previous = errorUtils.getGlobalHandler?.();
|
|
2452
|
+
errorUtils.setGlobalHandler((error, isFatal) => {
|
|
2453
|
+
if (this.isEnabled()) this.captureUnhandled(error);
|
|
2454
|
+
previous?.(error, isFatal);
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
/** @internal Test seam: ejercita el camino de persistencia (window handlers no son testeables en vitest). */
|
|
2458
|
+
_persistForTest(error) {
|
|
2459
|
+
this.captureUnhandled(error);
|
|
2460
|
+
}
|
|
2461
|
+
};
|
|
2462
|
+
|
|
2463
|
+
// src/analytics/messaging.ts
|
|
2464
|
+
function buildMessageParams(stage, o) {
|
|
2465
|
+
const p = {
|
|
2466
|
+
stage,
|
|
2467
|
+
message_id: o.id,
|
|
2468
|
+
message_title: o.title,
|
|
2469
|
+
channel: o.channel
|
|
2470
|
+
};
|
|
2471
|
+
if (o.campaign) p.campaign = o.campaign;
|
|
2472
|
+
if (o.value !== void 0) p.value = o.value;
|
|
2473
|
+
if (o.currency) p.currency = o.currency;
|
|
2474
|
+
return { ...p, ...o.params ?? {} };
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2321
2477
|
// src/core/deepdots-popups.ts
|
|
2322
2478
|
var EXIT_QUEUE_STORAGE_KEY = "__deepdots_exit_popup_queue__";
|
|
2323
2479
|
var DAY_IN_MS = 24 * 60 * 60 * 1e3;
|
|
2324
2480
|
var ANALYTICS_MAX_BATCH_SIZE = 20;
|
|
2325
2481
|
var ANALYTICS_FLUSH_INTERVAL_MS = 3e4;
|
|
2482
|
+
var RESERVED_EVENT_PREFIX = "deepdots_";
|
|
2483
|
+
var CUSTOM_EVENT_PREFIX = "deepdots_event_";
|
|
2326
2484
|
var DeepdotsPopups = class {
|
|
2327
2485
|
constructor() {
|
|
2328
2486
|
this.config = null;
|
|
@@ -2352,6 +2510,8 @@ var DeepdotsPopups = class {
|
|
|
2352
2510
|
this.analyticsFeedbackSessionId = void 0;
|
|
2353
2511
|
/** Timer del flush periódico de analytics (cada ANALYTICS_FLUSH_INTERVAL_MS). */
|
|
2354
2512
|
this.analyticsFlushTimer = void 0;
|
|
2513
|
+
/** Crash & error reporting (#14–17). Null hasta init(). */
|
|
2514
|
+
this.crashReporter = null;
|
|
2355
2515
|
/** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
|
|
2356
2516
|
this.navObserver = null;
|
|
2357
2517
|
/** Tiempo activo (engagement time, #8). */
|
|
@@ -2403,25 +2563,44 @@ var DeepdotsPopups = class {
|
|
|
2403
2563
|
this.analyticsFeedbackSessionId = id;
|
|
2404
2564
|
}
|
|
2405
2565
|
}) : void 0;
|
|
2566
|
+
const device = config.device ?? collectDeviceInfo(config.appVersion);
|
|
2406
2567
|
this.analytics = new AnalyticsManager({
|
|
2407
2568
|
sink: analyticsSink,
|
|
2408
2569
|
publicKey: config.analytics?.publicKey ?? this.config.apiKey,
|
|
2409
2570
|
language: typeof navigator !== "undefined" ? navigator.language : void 0,
|
|
2410
2571
|
platform: config.platform ?? "web",
|
|
2411
|
-
device
|
|
2572
|
+
device,
|
|
2412
2573
|
maxBatchSize: ANALYTICS_MAX_BATCH_SIZE,
|
|
2413
2574
|
onFlushNeeded: () => this.flushAnalytics()
|
|
2414
2575
|
});
|
|
2576
|
+
this.crashReporter = new CrashReporter({
|
|
2577
|
+
storage,
|
|
2578
|
+
emit: (params) => this.track("deepdots_app_crash", params),
|
|
2579
|
+
device: () => ({ appVersion: device.app_version, osVersion: device.os_version, deviceModel: device.device_model }),
|
|
2580
|
+
sessionId: () => this.tracking?.getSessionId() ?? null,
|
|
2581
|
+
now: () => Date.now(),
|
|
2582
|
+
enabled: () => this.tracking?.isTrackingEnabled() ?? false
|
|
2583
|
+
});
|
|
2584
|
+
if (this.tracking?.isTrackingEnabled()) {
|
|
2585
|
+
this.crashReporter.install();
|
|
2586
|
+
}
|
|
2415
2587
|
collectGeoInfo().then((geo) => {
|
|
2416
2588
|
if (geo) this.analytics?.updateDevice(geo);
|
|
2417
2589
|
}).catch(() => {
|
|
2418
2590
|
});
|
|
2419
2591
|
this.navObserver = new NavigationObserver();
|
|
2420
|
-
this.navObserver.onVisit((v) => this.track("
|
|
2592
|
+
this.navObserver.onVisit((v) => this.track("deepdots_page_view", { screen: v.screen, duration_seconds: v.durationSeconds }));
|
|
2421
2593
|
this.navObserver.install();
|
|
2422
2594
|
this.engagement = new EngagementTracker();
|
|
2423
2595
|
this.engagement.resume();
|
|
2424
2596
|
this.setupAnalyticsFlush();
|
|
2597
|
+
this.track("deepdots_session_start", {});
|
|
2598
|
+
const pendingCrashes = this.crashReporter.drainPendingCrashes();
|
|
2599
|
+
if (this.tracking?.isTrackingEnabled()) {
|
|
2600
|
+
for (const rec of pendingCrashes) {
|
|
2601
|
+
this.track("deepdots_app_crash", crashRecordToParams(rec));
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2425
2604
|
this.initialized = true;
|
|
2426
2605
|
this.log("SDK initialized", this.config);
|
|
2427
2606
|
if (this.renderer.init) this.renderer.init();
|
|
@@ -2454,14 +2633,25 @@ var DeepdotsPopups = class {
|
|
|
2454
2633
|
/** Registra un evento de analítica (modelo GA: nombre + params). No-op si tracking off. */
|
|
2455
2634
|
track(name, params) {
|
|
2456
2635
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
2457
|
-
|
|
2458
|
-
this.
|
|
2636
|
+
const eventName = name.startsWith(RESERVED_EVENT_PREFIX) ? name : `${CUSTOM_EVENT_PREFIX}${name}`;
|
|
2637
|
+
this.analytics?.track(eventName, params);
|
|
2638
|
+
this.log("analytics \xB7 track:", eventName, params ?? {});
|
|
2459
2639
|
}
|
|
2460
2640
|
/** User attributes para breakdowns (registration_status, pass_type, sector, pass_status…). */
|
|
2461
2641
|
setUserAttributes(attributes) {
|
|
2462
2642
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
2463
2643
|
this.analytics?.setUserAttributes(attributes);
|
|
2464
2644
|
}
|
|
2645
|
+
/** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
|
|
2646
|
+
reportError(error, options) {
|
|
2647
|
+
if (!this.tracking?.isTrackingEnabled()) return;
|
|
2648
|
+
this.crashReporter?.reportError(error, options);
|
|
2649
|
+
}
|
|
2650
|
+
/** Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. No-op si tracking off. */
|
|
2651
|
+
installReactNativeCrashHandler(errorUtils) {
|
|
2652
|
+
if (!this.tracking?.isTrackingEnabled()) return;
|
|
2653
|
+
this.crashReporter?.installReactNative(errorUtils);
|
|
2654
|
+
}
|
|
2465
2655
|
/**
|
|
2466
2656
|
* Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
|
|
2467
2657
|
* Contact del backend para segmentación/targeting de popups. Identificada por el `userId` del
|
|
@@ -2479,23 +2669,27 @@ var DeepdotsPopups = class {
|
|
|
2479
2669
|
this.analytics?.enterMiniService(name, entryPointType);
|
|
2480
2670
|
this.log("analytics \xB7 enterMiniService:", name, entryPointType ?? "");
|
|
2481
2671
|
}
|
|
2482
|
-
/** Cierra el mini-service
|
|
2483
|
-
exitMiniService() {
|
|
2672
|
+
/** Cierra el mini-service `name` (emite `mini_service_exit` con duración, #27). No-op si tracking off o si ese no está activo. */
|
|
2673
|
+
exitMiniService(name) {
|
|
2484
2674
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
2485
|
-
this.analytics?.exitMiniService();
|
|
2486
|
-
this.log("analytics \xB7 exitMiniService");
|
|
2675
|
+
this.analytics?.exitMiniService(name);
|
|
2676
|
+
this.log("analytics \xB7 exitMiniService:", name);
|
|
2487
2677
|
}
|
|
2488
2678
|
/** Findability (#31/#35): registra una búsqueda. `has_results` se deriva de `resultsCount`. */
|
|
2489
2679
|
trackSearch(query, resultsCount, params) {
|
|
2490
|
-
this.track("
|
|
2680
|
+
this.track("deepdots_search", { query, results_count: resultsCount, has_results: resultsCount > 0, ...params ?? {} });
|
|
2491
2681
|
}
|
|
2492
2682
|
/** Findability friction (#34/#35): señal de fricción con su `friction_topic`. */
|
|
2493
2683
|
trackFindabilityFriction(frictionTopic, params) {
|
|
2494
|
-
this.track("
|
|
2684
|
+
this.track("deepdots_findability_friction", { friction_topic: frictionTopic, ...params ?? {} });
|
|
2495
2685
|
}
|
|
2496
2686
|
/** Funnel: un paso del embudo, correlacionado por `taskId`. El backend reconstruye conversión/drop-off/tiempo. */
|
|
2497
2687
|
trackFunnelStep(funnel, step, taskId, params) {
|
|
2498
|
-
this.track("
|
|
2688
|
+
this.track("deepdots_funnel_step", { funnel, step, task_id: taskId, ...params ?? {} });
|
|
2689
|
+
}
|
|
2690
|
+
/** Messaging (#18–22): registra una etapa del funnel de una notificación (push/in-app). No-op si tracking off. */
|
|
2691
|
+
trackMessage(stage, options) {
|
|
2692
|
+
this.track("deepdots_message", buildMessageParams(stage, options));
|
|
2499
2693
|
}
|
|
2500
2694
|
/**
|
|
2501
2695
|
* Navegación MANUAL (entornos sin History API, p. ej. React Native con React Navigation).
|
|
@@ -2521,7 +2715,7 @@ var DeepdotsPopups = class {
|
|
|
2521
2715
|
onBackground() {
|
|
2522
2716
|
this.navObserver?.stop();
|
|
2523
2717
|
this.navStarted = false;
|
|
2524
|
-
this.
|
|
2718
|
+
if (this.tracking?.isTrackingEnabled()) this.analytics?.exitAllMiniServices();
|
|
2525
2719
|
this.flushEngagement();
|
|
2526
2720
|
this.engagement?.pause();
|
|
2527
2721
|
this.flushAnalytics();
|
|
@@ -2544,7 +2738,7 @@ var DeepdotsPopups = class {
|
|
|
2544
2738
|
flushEngagement() {
|
|
2545
2739
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
2546
2740
|
const ms = this.engagement?.consume() ?? 0;
|
|
2547
|
-
if (ms > 0) this.track("
|
|
2741
|
+
if (ms > 0) this.track("deepdots_user_engagement", { engagement_time_msec: ms });
|
|
2548
2742
|
}
|
|
2549
2743
|
analyticsIdentity() {
|
|
2550
2744
|
return {
|
|
@@ -2559,7 +2753,7 @@ var DeepdotsPopups = class {
|
|
|
2559
2753
|
window.addEventListener("pagehide", () => {
|
|
2560
2754
|
clearInterval(this.analyticsFlushTimer);
|
|
2561
2755
|
this.navObserver?.stop();
|
|
2562
|
-
this.
|
|
2756
|
+
if (this.tracking?.isTrackingEnabled()) this.analytics?.exitAllMiniServices();
|
|
2563
2757
|
this.flushEngagement();
|
|
2564
2758
|
this.flushAnalytics();
|
|
2565
2759
|
});
|
|
@@ -3225,6 +3419,10 @@ function setupReactNative(sdk2, config, deps = {}) {
|
|
|
3225
3419
|
const device = deps.deviceInfo ? collectRnDevice(deps.deviceInfo) : config.device;
|
|
3226
3420
|
const platform = deps.platform ?? config.platform;
|
|
3227
3421
|
sdk2.init({ ...config, storage, device, platform });
|
|
3422
|
+
const errorUtils = deps.errorUtils ?? globalThis.ErrorUtils;
|
|
3423
|
+
if (errorUtils && typeof errorUtils.setGlobalHandler === "function") {
|
|
3424
|
+
sdk2.installReactNativeCrashHandler(errorUtils);
|
|
3425
|
+
}
|
|
3228
3426
|
let subscription;
|
|
3229
3427
|
if (deps.appState?.addEventListener) {
|
|
3230
3428
|
subscription = deps.appState.addEventListener("change", (state) => {
|