@magicfeedback/popup-sdk 1.1.2 → 1.1.3
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 +90 -3
- package/dist/index.d.ts +90 -3
- package/dist/index.js +208 -29
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +208 -29
- package/dist/index.mjs.map +1 -1
- package/dist/react-native.d.mts +34 -2
- package/dist/react-native.d.ts +34 -2
- package/dist/react-native.js +206 -29
- package/dist/react-native.js.map +1 -1
- package/dist/react-native.mjs +206 -29
- package/dist/react-native.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -352,6 +352,85 @@ declare class ContactManager {
|
|
|
352
352
|
setAttributes(attributes: ContactAttributes): Promise<boolean>;
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
+
/**
|
|
356
|
+
* Crash & error reporting (Stability #14–17) — captura de errores GESTIONADOS.
|
|
357
|
+
*
|
|
358
|
+
* - `reportError()`: API pública para el host (app viva) → emite `deepdots_app_crash` ya.
|
|
359
|
+
* - handlers de `window` (errores no capturados): persisten a disco y se reenvían
|
|
360
|
+
* en el siguiente arranque (el proceso puede morir antes del flush normal).
|
|
361
|
+
*
|
|
362
|
+
* El contexto de device/sesión se captura EN EL MOMENTO del crash y se guarda en el
|
|
363
|
+
* record — no se lee del envelope en el replay (el replay puede ocurrir en otra
|
|
364
|
+
* app_version/OS). Las dims OS/device en Web suelen ir vacías (se derivan del UA en backend).
|
|
365
|
+
*/
|
|
366
|
+
|
|
367
|
+
type CrashSeverity = 'fatal' | 'error' | 'warning';
|
|
368
|
+
interface CrashRecord {
|
|
369
|
+
crashedAt: number;
|
|
370
|
+
crashType: string;
|
|
371
|
+
message: string;
|
|
372
|
+
stack: string;
|
|
373
|
+
fatal: boolean;
|
|
374
|
+
handled: boolean;
|
|
375
|
+
severity: CrashSeverity;
|
|
376
|
+
sessionId: string | null;
|
|
377
|
+
appVersion?: string;
|
|
378
|
+
osVersion?: string;
|
|
379
|
+
deviceModel?: string;
|
|
380
|
+
context?: Record<string, string>;
|
|
381
|
+
}
|
|
382
|
+
interface ReportErrorOptions {
|
|
383
|
+
severity?: CrashSeverity;
|
|
384
|
+
handled?: boolean;
|
|
385
|
+
context?: Record<string, unknown>;
|
|
386
|
+
}
|
|
387
|
+
interface DeviceSnapshot {
|
|
388
|
+
appVersion?: string;
|
|
389
|
+
osVersion?: string;
|
|
390
|
+
deviceModel?: string;
|
|
391
|
+
}
|
|
392
|
+
/** Forma mínima de `global.ErrorUtils` de React Native. */
|
|
393
|
+
interface ReactNativeErrorUtils {
|
|
394
|
+
getGlobalHandler?(): ((error: unknown, isFatal?: boolean) => void) | undefined;
|
|
395
|
+
setGlobalHandler(handler: (error: unknown, isFatal?: boolean) => void): void;
|
|
396
|
+
}
|
|
397
|
+
interface CrashReporterOptions {
|
|
398
|
+
storage: KeyValueStorage;
|
|
399
|
+
/** Emite un evento deepdots_app_crash AHORA (app viva). */
|
|
400
|
+
emit: (params: Record<string, unknown>) => void;
|
|
401
|
+
/** Snapshot de device en el momento del crash. */
|
|
402
|
+
device: () => DeviceSnapshot;
|
|
403
|
+
/** session_id en el momento del crash. */
|
|
404
|
+
sessionId: () => string | null;
|
|
405
|
+
now?: () => number;
|
|
406
|
+
/** Kill-switch de consentimiento. */
|
|
407
|
+
enabled?: () => boolean;
|
|
408
|
+
}
|
|
409
|
+
/** Convierte un CrashRecord en los params del evento `deepdots_app_crash` (omite undefined). */
|
|
410
|
+
declare function crashRecordToParams(r: CrashRecord): Record<string, unknown>;
|
|
411
|
+
declare class CrashReporter {
|
|
412
|
+
private options;
|
|
413
|
+
private now;
|
|
414
|
+
constructor(options: CrashReporterOptions);
|
|
415
|
+
private isEnabled;
|
|
416
|
+
/** Construye un CrashRecord capturando contexto en el momento del crash. */
|
|
417
|
+
private buildRecord;
|
|
418
|
+
/** API pública del host: reporta un error (app viva) → emite el evento ya. */
|
|
419
|
+
reportError(error: unknown, options?: ReportErrorOptions): void;
|
|
420
|
+
/** Captura un error no manejado (fatal) → persiste a disco para replay en el siguiente arranque. */
|
|
421
|
+
captureUnhandled(error: unknown): void;
|
|
422
|
+
/** Persiste un crash no capturado a disco para reenviarlo en el siguiente arranque. */
|
|
423
|
+
private persist;
|
|
424
|
+
/** Lee y vacía la cola de crashes pendientes (llamado en init para el replay). */
|
|
425
|
+
drainPendingCrashes(): CrashRecord[];
|
|
426
|
+
/** Instala los handlers globales de errores no capturados (no-op sin `window`). */
|
|
427
|
+
install(): void;
|
|
428
|
+
/** Engancha `global.ErrorUtils` de RN (no hay `window`): persiste y encadena al handler previo. */
|
|
429
|
+
installReactNative(errorUtils: ReactNativeErrorUtils): void;
|
|
430
|
+
/** @internal Test seam: ejercita el camino de persistencia (window handlers no son testeables en vitest). */
|
|
431
|
+
_persistForTest(error: unknown): void;
|
|
432
|
+
}
|
|
433
|
+
|
|
355
434
|
/**
|
|
356
435
|
* Main class for managing survey popups
|
|
357
436
|
*/
|
|
@@ -380,6 +459,8 @@ declare class DeepdotsPopups {
|
|
|
380
459
|
private analyticsFeedbackSessionId;
|
|
381
460
|
/** Timer del flush periódico de analytics (cada ANALYTICS_FLUSH_INTERVAL_MS). */
|
|
382
461
|
private analyticsFlushTimer;
|
|
462
|
+
/** Crash & error reporting (#14–17). Null hasta init(). */
|
|
463
|
+
private crashReporter;
|
|
383
464
|
/** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
|
|
384
465
|
private navObserver;
|
|
385
466
|
/** Tiempo activo (engagement time, #8). */
|
|
@@ -400,6 +481,10 @@ declare class DeepdotsPopups {
|
|
|
400
481
|
track(name: string, params?: Record<string, unknown>): void;
|
|
401
482
|
/** User attributes para breakdowns (registration_status, pass_type, sector, pass_status…). */
|
|
402
483
|
setUserAttributes(attributes: Record<string, string | number | boolean>): void;
|
|
484
|
+
/** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
|
|
485
|
+
reportError(error: unknown, options?: ReportErrorOptions): void;
|
|
486
|
+
/** Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. No-op si tracking off. */
|
|
487
|
+
installReactNativeCrashHandler(errorUtils: ReactNativeErrorUtils): void;
|
|
403
488
|
/**
|
|
404
489
|
* Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
|
|
405
490
|
* Contact del backend para segmentación/targeting de popups. Identificada por el `userId` del
|
|
@@ -409,8 +494,8 @@ declare class DeepdotsPopups {
|
|
|
409
494
|
setContactAttributes(attributes: ContactAttributes): Promise<boolean>;
|
|
410
495
|
/** Marca el inicio de un mini-service; etiqueta los eventos siguientes. No-op si tracking off. */
|
|
411
496
|
enterMiniService(name: string, entryPointType?: string): void;
|
|
412
|
-
/** Cierra el mini-service
|
|
413
|
-
exitMiniService(): void;
|
|
497
|
+
/** Cierra el mini-service `name` (emite `mini_service_exit` con duración, #27). No-op si tracking off o si ese no está activo. */
|
|
498
|
+
exitMiniService(name: string): void;
|
|
414
499
|
/** Findability (#31/#35): registra una búsqueda. `has_results` se deriva de `resultsCount`. */
|
|
415
500
|
trackSearch(query: string, resultsCount: number, params?: Record<string, unknown>): void;
|
|
416
501
|
/** Findability friction (#34/#35): señal de fricción con su `friction_topic`. */
|
|
@@ -610,6 +695,8 @@ interface ReactNativeSetupDeps {
|
|
|
610
695
|
appState?: AppStateLike | null;
|
|
611
696
|
platform?: 'web' | 'android' | 'ios';
|
|
612
697
|
renderer?: PopupRenderer;
|
|
698
|
+
/** `global.ErrorUtils` de RN (para capturar errores JS no manejados). Default: `globalThis.ErrorUtils`. */
|
|
699
|
+
errorUtils?: ReactNativeErrorUtils | null;
|
|
613
700
|
}
|
|
614
701
|
/** Adaptador KeyValueStorage (síncrono) sobre una instancia de MMKV. */
|
|
615
702
|
declare function mmkvStorage(mmkv: MmkvLike): KeyValueStorage;
|
|
@@ -622,4 +709,4 @@ declare function collectRnDevice(d: DeviceInfoLike): DeviceInfo;
|
|
|
622
709
|
*/
|
|
623
710
|
declare function setupReactNative(sdk: DeepdotsPopups, config: DeepdotsInitParams, deps?: ReactNativeSetupDeps): () => void;
|
|
624
711
|
|
|
625
|
-
export { type AnalyticsFeedbackBody, type AnalyticsKeys, type AppStateLike, BrowserPopupRenderer, type ContactAttributeValue, type ContactAttributes, type ContactBody, ContactManager, type DeepdotsConfig, type DeepdotsEvent, type DeepdotsEventType, DeepdotsPopups, type DeviceInfo, type DeviceInfoLike, type EventListener, type FeedbackKV, InMemoryStorage, type KeyValueStorage, type MmkvLike, NoopPopupRenderer, POPUP_TRIGGER_CONDITION_STATUSES, type PopupDefinition, type PopupRenderer, type PopupTrigger, type PopupTriggerCondition, type PopupTriggerConditionStatus, ReactNativePopupRenderer, type ReactNativeRendererOptions, type ReactNativeSetupDeps, type ReactNativeSurveyPayload, type TriggerConfig, buildAnalyticsFeedbackBody, buildSurveyHtml, collectRnDevice, createDefaultRenderer, createFeedbackSink, mmkvStorage, setupReactNative };
|
|
712
|
+
export { type AnalyticsFeedbackBody, type AnalyticsKeys, type AppStateLike, BrowserPopupRenderer, type ContactAttributeValue, type ContactAttributes, type ContactBody, ContactManager, type CrashRecord, CrashReporter, type CrashSeverity, type DeepdotsConfig, type DeepdotsEvent, type DeepdotsEventType, DeepdotsPopups, type DeviceInfo, type DeviceInfoLike, type DeviceSnapshot, type EventListener, type FeedbackKV, InMemoryStorage, type KeyValueStorage, type MmkvLike, NoopPopupRenderer, POPUP_TRIGGER_CONDITION_STATUSES, type PopupDefinition, type PopupRenderer, type PopupTrigger, type PopupTriggerCondition, type PopupTriggerConditionStatus, type ReactNativeErrorUtils, ReactNativePopupRenderer, type ReactNativeRendererOptions, type ReactNativeSetupDeps, type ReactNativeSurveyPayload, type ReportErrorOptions, type TriggerConfig, buildAnalyticsFeedbackBody, buildSurveyHtml, collectRnDevice, crashRecordToParams, createDefaultRenderer, createFeedbackSink, mmkvStorage, setupReactNative };
|
package/dist/index.d.ts
CHANGED
|
@@ -352,6 +352,85 @@ declare class ContactManager {
|
|
|
352
352
|
setAttributes(attributes: ContactAttributes): Promise<boolean>;
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
+
/**
|
|
356
|
+
* Crash & error reporting (Stability #14–17) — captura de errores GESTIONADOS.
|
|
357
|
+
*
|
|
358
|
+
* - `reportError()`: API pública para el host (app viva) → emite `deepdots_app_crash` ya.
|
|
359
|
+
* - handlers de `window` (errores no capturados): persisten a disco y se reenvían
|
|
360
|
+
* en el siguiente arranque (el proceso puede morir antes del flush normal).
|
|
361
|
+
*
|
|
362
|
+
* El contexto de device/sesión se captura EN EL MOMENTO del crash y se guarda en el
|
|
363
|
+
* record — no se lee del envelope en el replay (el replay puede ocurrir en otra
|
|
364
|
+
* app_version/OS). Las dims OS/device en Web suelen ir vacías (se derivan del UA en backend).
|
|
365
|
+
*/
|
|
366
|
+
|
|
367
|
+
type CrashSeverity = 'fatal' | 'error' | 'warning';
|
|
368
|
+
interface CrashRecord {
|
|
369
|
+
crashedAt: number;
|
|
370
|
+
crashType: string;
|
|
371
|
+
message: string;
|
|
372
|
+
stack: string;
|
|
373
|
+
fatal: boolean;
|
|
374
|
+
handled: boolean;
|
|
375
|
+
severity: CrashSeverity;
|
|
376
|
+
sessionId: string | null;
|
|
377
|
+
appVersion?: string;
|
|
378
|
+
osVersion?: string;
|
|
379
|
+
deviceModel?: string;
|
|
380
|
+
context?: Record<string, string>;
|
|
381
|
+
}
|
|
382
|
+
interface ReportErrorOptions {
|
|
383
|
+
severity?: CrashSeverity;
|
|
384
|
+
handled?: boolean;
|
|
385
|
+
context?: Record<string, unknown>;
|
|
386
|
+
}
|
|
387
|
+
interface DeviceSnapshot {
|
|
388
|
+
appVersion?: string;
|
|
389
|
+
osVersion?: string;
|
|
390
|
+
deviceModel?: string;
|
|
391
|
+
}
|
|
392
|
+
/** Forma mínima de `global.ErrorUtils` de React Native. */
|
|
393
|
+
interface ReactNativeErrorUtils {
|
|
394
|
+
getGlobalHandler?(): ((error: unknown, isFatal?: boolean) => void) | undefined;
|
|
395
|
+
setGlobalHandler(handler: (error: unknown, isFatal?: boolean) => void): void;
|
|
396
|
+
}
|
|
397
|
+
interface CrashReporterOptions {
|
|
398
|
+
storage: KeyValueStorage;
|
|
399
|
+
/** Emite un evento deepdots_app_crash AHORA (app viva). */
|
|
400
|
+
emit: (params: Record<string, unknown>) => void;
|
|
401
|
+
/** Snapshot de device en el momento del crash. */
|
|
402
|
+
device: () => DeviceSnapshot;
|
|
403
|
+
/** session_id en el momento del crash. */
|
|
404
|
+
sessionId: () => string | null;
|
|
405
|
+
now?: () => number;
|
|
406
|
+
/** Kill-switch de consentimiento. */
|
|
407
|
+
enabled?: () => boolean;
|
|
408
|
+
}
|
|
409
|
+
/** Convierte un CrashRecord en los params del evento `deepdots_app_crash` (omite undefined). */
|
|
410
|
+
declare function crashRecordToParams(r: CrashRecord): Record<string, unknown>;
|
|
411
|
+
declare class CrashReporter {
|
|
412
|
+
private options;
|
|
413
|
+
private now;
|
|
414
|
+
constructor(options: CrashReporterOptions);
|
|
415
|
+
private isEnabled;
|
|
416
|
+
/** Construye un CrashRecord capturando contexto en el momento del crash. */
|
|
417
|
+
private buildRecord;
|
|
418
|
+
/** API pública del host: reporta un error (app viva) → emite el evento ya. */
|
|
419
|
+
reportError(error: unknown, options?: ReportErrorOptions): void;
|
|
420
|
+
/** Captura un error no manejado (fatal) → persiste a disco para replay en el siguiente arranque. */
|
|
421
|
+
captureUnhandled(error: unknown): void;
|
|
422
|
+
/** Persiste un crash no capturado a disco para reenviarlo en el siguiente arranque. */
|
|
423
|
+
private persist;
|
|
424
|
+
/** Lee y vacía la cola de crashes pendientes (llamado en init para el replay). */
|
|
425
|
+
drainPendingCrashes(): CrashRecord[];
|
|
426
|
+
/** Instala los handlers globales de errores no capturados (no-op sin `window`). */
|
|
427
|
+
install(): void;
|
|
428
|
+
/** Engancha `global.ErrorUtils` de RN (no hay `window`): persiste y encadena al handler previo. */
|
|
429
|
+
installReactNative(errorUtils: ReactNativeErrorUtils): void;
|
|
430
|
+
/** @internal Test seam: ejercita el camino de persistencia (window handlers no son testeables en vitest). */
|
|
431
|
+
_persistForTest(error: unknown): void;
|
|
432
|
+
}
|
|
433
|
+
|
|
355
434
|
/**
|
|
356
435
|
* Main class for managing survey popups
|
|
357
436
|
*/
|
|
@@ -380,6 +459,8 @@ declare class DeepdotsPopups {
|
|
|
380
459
|
private analyticsFeedbackSessionId;
|
|
381
460
|
/** Timer del flush periódico de analytics (cada ANALYTICS_FLUSH_INTERVAL_MS). */
|
|
382
461
|
private analyticsFlushTimer;
|
|
462
|
+
/** Crash & error reporting (#14–17). Null hasta init(). */
|
|
463
|
+
private crashReporter;
|
|
383
464
|
/** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
|
|
384
465
|
private navObserver;
|
|
385
466
|
/** Tiempo activo (engagement time, #8). */
|
|
@@ -400,6 +481,10 @@ declare class DeepdotsPopups {
|
|
|
400
481
|
track(name: string, params?: Record<string, unknown>): void;
|
|
401
482
|
/** User attributes para breakdowns (registration_status, pass_type, sector, pass_status…). */
|
|
402
483
|
setUserAttributes(attributes: Record<string, string | number | boolean>): void;
|
|
484
|
+
/** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
|
|
485
|
+
reportError(error: unknown, options?: ReportErrorOptions): void;
|
|
486
|
+
/** Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. No-op si tracking off. */
|
|
487
|
+
installReactNativeCrashHandler(errorUtils: ReactNativeErrorUtils): void;
|
|
403
488
|
/**
|
|
404
489
|
* Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
|
|
405
490
|
* Contact del backend para segmentación/targeting de popups. Identificada por el `userId` del
|
|
@@ -409,8 +494,8 @@ declare class DeepdotsPopups {
|
|
|
409
494
|
setContactAttributes(attributes: ContactAttributes): Promise<boolean>;
|
|
410
495
|
/** Marca el inicio de un mini-service; etiqueta los eventos siguientes. No-op si tracking off. */
|
|
411
496
|
enterMiniService(name: string, entryPointType?: string): void;
|
|
412
|
-
/** Cierra el mini-service
|
|
413
|
-
exitMiniService(): void;
|
|
497
|
+
/** Cierra el mini-service `name` (emite `mini_service_exit` con duración, #27). No-op si tracking off o si ese no está activo. */
|
|
498
|
+
exitMiniService(name: string): void;
|
|
414
499
|
/** Findability (#31/#35): registra una búsqueda. `has_results` se deriva de `resultsCount`. */
|
|
415
500
|
trackSearch(query: string, resultsCount: number, params?: Record<string, unknown>): void;
|
|
416
501
|
/** Findability friction (#34/#35): señal de fricción con su `friction_topic`. */
|
|
@@ -610,6 +695,8 @@ interface ReactNativeSetupDeps {
|
|
|
610
695
|
appState?: AppStateLike | null;
|
|
611
696
|
platform?: 'web' | 'android' | 'ios';
|
|
612
697
|
renderer?: PopupRenderer;
|
|
698
|
+
/** `global.ErrorUtils` de RN (para capturar errores JS no manejados). Default: `globalThis.ErrorUtils`. */
|
|
699
|
+
errorUtils?: ReactNativeErrorUtils | null;
|
|
613
700
|
}
|
|
614
701
|
/** Adaptador KeyValueStorage (síncrono) sobre una instancia de MMKV. */
|
|
615
702
|
declare function mmkvStorage(mmkv: MmkvLike): KeyValueStorage;
|
|
@@ -622,4 +709,4 @@ declare function collectRnDevice(d: DeviceInfoLike): DeviceInfo;
|
|
|
622
709
|
*/
|
|
623
710
|
declare function setupReactNative(sdk: DeepdotsPopups, config: DeepdotsInitParams, deps?: ReactNativeSetupDeps): () => void;
|
|
624
711
|
|
|
625
|
-
export { type AnalyticsFeedbackBody, type AnalyticsKeys, type AppStateLike, BrowserPopupRenderer, type ContactAttributeValue, type ContactAttributes, type ContactBody, ContactManager, type DeepdotsConfig, type DeepdotsEvent, type DeepdotsEventType, DeepdotsPopups, type DeviceInfo, type DeviceInfoLike, type EventListener, type FeedbackKV, InMemoryStorage, type KeyValueStorage, type MmkvLike, NoopPopupRenderer, POPUP_TRIGGER_CONDITION_STATUSES, type PopupDefinition, type PopupRenderer, type PopupTrigger, type PopupTriggerCondition, type PopupTriggerConditionStatus, ReactNativePopupRenderer, type ReactNativeRendererOptions, type ReactNativeSetupDeps, type ReactNativeSurveyPayload, type TriggerConfig, buildAnalyticsFeedbackBody, buildSurveyHtml, collectRnDevice, createDefaultRenderer, createFeedbackSink, mmkvStorage, setupReactNative };
|
|
712
|
+
export { type AnalyticsFeedbackBody, type AnalyticsKeys, type AppStateLike, BrowserPopupRenderer, type ContactAttributeValue, type ContactAttributes, type ContactBody, ContactManager, type CrashRecord, CrashReporter, type CrashSeverity, type DeepdotsConfig, type DeepdotsEvent, type DeepdotsEventType, DeepdotsPopups, type DeviceInfo, type DeviceInfoLike, type DeviceSnapshot, type EventListener, type FeedbackKV, InMemoryStorage, type KeyValueStorage, type MmkvLike, NoopPopupRenderer, POPUP_TRIGGER_CONDITION_STATUSES, type PopupDefinition, type PopupRenderer, type PopupTrigger, type PopupTriggerCondition, type PopupTriggerConditionStatus, type ReactNativeErrorUtils, ReactNativePopupRenderer, type ReactNativeRendererOptions, type ReactNativeSetupDeps, type ReactNativeSurveyPayload, type ReportErrorOptions, type TriggerConfig, buildAnalyticsFeedbackBody, buildSurveyHtml, collectRnDevice, crashRecordToParams, createDefaultRenderer, createFeedbackSink, mmkvStorage, setupReactNative };
|
package/dist/index.js
CHANGED
|
@@ -3458,6 +3458,7 @@ var index_exports = {};
|
|
|
3458
3458
|
__export(index_exports, {
|
|
3459
3459
|
BrowserPopupRenderer: () => BrowserPopupRenderer,
|
|
3460
3460
|
ContactManager: () => ContactManager,
|
|
3461
|
+
CrashReporter: () => CrashReporter,
|
|
3461
3462
|
DeepdotsPopups: () => DeepdotsPopups,
|
|
3462
3463
|
InMemoryStorage: () => InMemoryStorage,
|
|
3463
3464
|
NoopPopupRenderer: () => NoopPopupRenderer,
|
|
@@ -3466,6 +3467,7 @@ __export(index_exports, {
|
|
|
3466
3467
|
buildAnalyticsFeedbackBody: () => buildAnalyticsFeedbackBody,
|
|
3467
3468
|
buildSurveyHtml: () => buildSurveyHtml,
|
|
3468
3469
|
collectRnDevice: () => collectRnDevice,
|
|
3470
|
+
crashRecordToParams: () => crashRecordToParams,
|
|
3469
3471
|
createDefaultRenderer: () => createDefaultRenderer,
|
|
3470
3472
|
createFeedbackSink: () => createFeedbackSink,
|
|
3471
3473
|
mmkvStorage: () => mmkvStorage,
|
|
@@ -3860,8 +3862,8 @@ var AnalyticsManager = class {
|
|
|
3860
3862
|
constructor(options = {}) {
|
|
3861
3863
|
this.events = [];
|
|
3862
3864
|
this.attributes = {};
|
|
3863
|
-
|
|
3864
|
-
this.
|
|
3865
|
+
/** Mini-services activos: nombre → timestamp de entrada. El orden de inserción marca el "más reciente". */
|
|
3866
|
+
this.activeMiniServices = /* @__PURE__ */ new Map();
|
|
3865
3867
|
this.sink = options.sink ?? dryRunSink;
|
|
3866
3868
|
this.now = options.now ?? (() => Date.now());
|
|
3867
3869
|
this.publicKey = options.publicKey;
|
|
@@ -3882,24 +3884,38 @@ var AnalyticsManager = class {
|
|
|
3882
3884
|
this.attributes[k] = String(v);
|
|
3883
3885
|
}
|
|
3884
3886
|
}
|
|
3885
|
-
/**
|
|
3887
|
+
/**
|
|
3888
|
+
* Marca el inicio de un mini-service; etiqueta los eventos siguientes con `mini_service`.
|
|
3889
|
+
* Admite varios activos a la vez (concurrencia); el "actual" para etiquetar es el más reciente.
|
|
3890
|
+
* Reentrar con un nombre ya activo refresca su orden y su tiempo de entrada.
|
|
3891
|
+
*/
|
|
3886
3892
|
enterMiniService(name, entryPointType) {
|
|
3887
|
-
this.
|
|
3888
|
-
this.
|
|
3889
|
-
this.track("
|
|
3890
|
-
}
|
|
3891
|
-
/**
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3893
|
+
this.activeMiniServices.delete(name);
|
|
3894
|
+
this.activeMiniServices.set(name, this.now());
|
|
3895
|
+
this.track("deepdots_mini_service_enter", { entry_point_type: entryPointType ?? null });
|
|
3896
|
+
}
|
|
3897
|
+
/**
|
|
3898
|
+
* Cierra el mini-service `name` emitiendo `mini_service_exit` con su duración (#27).
|
|
3899
|
+
* No-op si ese nombre no está activo. Cierre coherente con concurrencia.
|
|
3900
|
+
*/
|
|
3901
|
+
exitMiniService(name) {
|
|
3902
|
+
const enteredAt = this.activeMiniServices.get(name);
|
|
3903
|
+
if (enteredAt === void 0) return;
|
|
3904
|
+
const durationSeconds = Math.max(0, Math.round((this.now() - enteredAt) / 1e3));
|
|
3905
|
+
this.activeMiniServices.delete(name);
|
|
3906
|
+
this.track("deepdots_mini_service_exit", { mini_service: name, duration_seconds: durationSeconds });
|
|
3907
|
+
}
|
|
3908
|
+
/** Cierra TODOS los mini-services activos (orden LIFO). Para el cierre por lifecycle (background/cierre). */
|
|
3909
|
+
exitAllMiniServices() {
|
|
3910
|
+
for (const name of Array.from(this.activeMiniServices.keys()).reverse()) {
|
|
3911
|
+
this.exitMiniService(name);
|
|
3912
|
+
}
|
|
3898
3913
|
}
|
|
3899
3914
|
/** Registra un evento de analítica (modelo GA: nombre + params). */
|
|
3900
3915
|
track(name, params) {
|
|
3916
|
+
const current = this.getMiniService();
|
|
3901
3917
|
const merged = {
|
|
3902
|
-
...
|
|
3918
|
+
...current ? { mini_service: current } : {},
|
|
3903
3919
|
...params ?? {}
|
|
3904
3920
|
};
|
|
3905
3921
|
this.events.push({
|
|
@@ -3911,9 +3927,11 @@ var AnalyticsManager = class {
|
|
|
3911
3927
|
this.onFlushNeeded?.();
|
|
3912
3928
|
}
|
|
3913
3929
|
}
|
|
3914
|
-
/** Mini-service activo
|
|
3930
|
+
/** Mini-service actual (el más reciente aún activo) para etiquetar eventos + metadata del survey (#33). */
|
|
3915
3931
|
getMiniService() {
|
|
3916
|
-
|
|
3932
|
+
let last = null;
|
|
3933
|
+
for (const name of this.activeMiniServices.keys()) last = name;
|
|
3934
|
+
return last;
|
|
3917
3935
|
}
|
|
3918
3936
|
/** Nº de eventos pendientes de flush. */
|
|
3919
3937
|
pending() {
|
|
@@ -4253,6 +4271,132 @@ var ContactManager = class {
|
|
|
4253
4271
|
}
|
|
4254
4272
|
};
|
|
4255
4273
|
|
|
4274
|
+
// src/analytics/crash-reporter.ts
|
|
4275
|
+
var STACK_MAX = 8e3;
|
|
4276
|
+
var QUEUE_KEY = "deepdots.crash.queue";
|
|
4277
|
+
var MAX_QUEUED = 20;
|
|
4278
|
+
function crashRecordToParams(r) {
|
|
4279
|
+
const params = {
|
|
4280
|
+
crashed_at: r.crashedAt,
|
|
4281
|
+
crash_type: r.crashType,
|
|
4282
|
+
message: r.message,
|
|
4283
|
+
stack: r.stack,
|
|
4284
|
+
fatal: r.fatal,
|
|
4285
|
+
handled: r.handled,
|
|
4286
|
+
severity: r.severity
|
|
4287
|
+
};
|
|
4288
|
+
if (r.sessionId) params.crashed_session_id = r.sessionId;
|
|
4289
|
+
if (r.appVersion) params.crashed_app_version = r.appVersion;
|
|
4290
|
+
if (r.osVersion) params.crashed_os_version = r.osVersion;
|
|
4291
|
+
if (r.deviceModel) params.crashed_device_model = r.deviceModel;
|
|
4292
|
+
if (r.context) {
|
|
4293
|
+
for (const [k, v] of Object.entries(r.context)) params[`ctx_${k}`] = v;
|
|
4294
|
+
}
|
|
4295
|
+
return params;
|
|
4296
|
+
}
|
|
4297
|
+
var CrashReporter = class {
|
|
4298
|
+
constructor(options) {
|
|
4299
|
+
this.options = options;
|
|
4300
|
+
this.now = options.now ?? (() => Date.now());
|
|
4301
|
+
}
|
|
4302
|
+
isEnabled() {
|
|
4303
|
+
return this.options.enabled ? this.options.enabled() : true;
|
|
4304
|
+
}
|
|
4305
|
+
/** Construye un CrashRecord capturando contexto en el momento del crash. */
|
|
4306
|
+
buildRecord(error, severity, handled, fatal, context) {
|
|
4307
|
+
const err = error;
|
|
4308
|
+
const isErr = error instanceof Error;
|
|
4309
|
+
const dev = this.options.device();
|
|
4310
|
+
const ctx = context ? Object.fromEntries(Object.entries(context).map(([k, v]) => [k, String(v)])) : void 0;
|
|
4311
|
+
return {
|
|
4312
|
+
crashedAt: this.now(),
|
|
4313
|
+
crashType: isErr && err.name || "Error",
|
|
4314
|
+
message: isErr ? String(err.message ?? "") : String(error),
|
|
4315
|
+
stack: (isErr && typeof err.stack === "string" ? err.stack : "").slice(0, STACK_MAX),
|
|
4316
|
+
fatal,
|
|
4317
|
+
handled,
|
|
4318
|
+
severity,
|
|
4319
|
+
sessionId: this.options.sessionId(),
|
|
4320
|
+
appVersion: dev.appVersion,
|
|
4321
|
+
osVersion: dev.osVersion,
|
|
4322
|
+
deviceModel: dev.deviceModel,
|
|
4323
|
+
context: ctx
|
|
4324
|
+
};
|
|
4325
|
+
}
|
|
4326
|
+
/** API pública del host: reporta un error (app viva) → emite el evento ya. */
|
|
4327
|
+
reportError(error, options = {}) {
|
|
4328
|
+
if (!this.isEnabled()) return;
|
|
4329
|
+
const severity = options.severity ?? "error";
|
|
4330
|
+
const handled = options.handled ?? true;
|
|
4331
|
+
const fatal = severity === "fatal";
|
|
4332
|
+
const record = this.buildRecord(error, severity, handled, fatal, options.context);
|
|
4333
|
+
this.options.emit(crashRecordToParams(record));
|
|
4334
|
+
}
|
|
4335
|
+
/** Captura un error no manejado (fatal) → persiste a disco para replay en el siguiente arranque. */
|
|
4336
|
+
captureUnhandled(error) {
|
|
4337
|
+
this.persist(this.buildRecord(error, "fatal", false, true));
|
|
4338
|
+
}
|
|
4339
|
+
/** Persiste un crash no capturado a disco para reenviarlo en el siguiente arranque. */
|
|
4340
|
+
persist(record) {
|
|
4341
|
+
let queue = [];
|
|
4342
|
+
try {
|
|
4343
|
+
const raw = this.options.storage.getItem(QUEUE_KEY);
|
|
4344
|
+
if (raw) queue = JSON.parse(raw);
|
|
4345
|
+
if (!Array.isArray(queue)) queue = [];
|
|
4346
|
+
} catch {
|
|
4347
|
+
queue = [];
|
|
4348
|
+
}
|
|
4349
|
+
queue.push(record);
|
|
4350
|
+
if (queue.length > MAX_QUEUED) queue = queue.slice(queue.length - MAX_QUEUED);
|
|
4351
|
+
try {
|
|
4352
|
+
this.options.storage.setItem(QUEUE_KEY, JSON.stringify(queue));
|
|
4353
|
+
} catch {
|
|
4354
|
+
}
|
|
4355
|
+
}
|
|
4356
|
+
/** Lee y vacía la cola de crashes pendientes (llamado en init para el replay). */
|
|
4357
|
+
drainPendingCrashes() {
|
|
4358
|
+
let queue = [];
|
|
4359
|
+
try {
|
|
4360
|
+
const raw = this.options.storage.getItem(QUEUE_KEY);
|
|
4361
|
+
if (raw) queue = JSON.parse(raw);
|
|
4362
|
+
if (!Array.isArray(queue)) queue = [];
|
|
4363
|
+
} catch {
|
|
4364
|
+
queue = [];
|
|
4365
|
+
}
|
|
4366
|
+
if (queue.length) {
|
|
4367
|
+
try {
|
|
4368
|
+
this.options.storage.removeItem(QUEUE_KEY);
|
|
4369
|
+
} catch {
|
|
4370
|
+
}
|
|
4371
|
+
}
|
|
4372
|
+
return queue;
|
|
4373
|
+
}
|
|
4374
|
+
/** Instala los handlers globales de errores no capturados (no-op sin `window`). */
|
|
4375
|
+
install() {
|
|
4376
|
+
if (typeof window === "undefined") return;
|
|
4377
|
+
window.addEventListener("error", (e) => {
|
|
4378
|
+
if (!this.isEnabled()) return;
|
|
4379
|
+
this.captureUnhandled(e.error ?? e.message ?? "error");
|
|
4380
|
+
});
|
|
4381
|
+
window.addEventListener("unhandledrejection", (e) => {
|
|
4382
|
+
if (!this.isEnabled()) return;
|
|
4383
|
+
this.captureUnhandled(e.reason ?? "unhandledrejection");
|
|
4384
|
+
});
|
|
4385
|
+
}
|
|
4386
|
+
/** Engancha `global.ErrorUtils` de RN (no hay `window`): persiste y encadena al handler previo. */
|
|
4387
|
+
installReactNative(errorUtils) {
|
|
4388
|
+
const previous = errorUtils.getGlobalHandler?.();
|
|
4389
|
+
errorUtils.setGlobalHandler((error, isFatal) => {
|
|
4390
|
+
if (this.isEnabled()) this.captureUnhandled(error);
|
|
4391
|
+
previous?.(error, isFatal);
|
|
4392
|
+
});
|
|
4393
|
+
}
|
|
4394
|
+
/** @internal Test seam: ejercita el camino de persistencia (window handlers no son testeables en vitest). */
|
|
4395
|
+
_persistForTest(error) {
|
|
4396
|
+
this.captureUnhandled(error);
|
|
4397
|
+
}
|
|
4398
|
+
};
|
|
4399
|
+
|
|
4256
4400
|
// src/core/deepdots-popups.ts
|
|
4257
4401
|
var EXIT_QUEUE_STORAGE_KEY = "__deepdots_exit_popup_queue__";
|
|
4258
4402
|
var DAY_IN_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -4287,6 +4431,8 @@ var DeepdotsPopups = class {
|
|
|
4287
4431
|
this.analyticsFeedbackSessionId = void 0;
|
|
4288
4432
|
/** Timer del flush periódico de analytics (cada ANALYTICS_FLUSH_INTERVAL_MS). */
|
|
4289
4433
|
this.analyticsFlushTimer = void 0;
|
|
4434
|
+
/** Crash & error reporting (#14–17). Null hasta init(). */
|
|
4435
|
+
this.crashReporter = null;
|
|
4290
4436
|
/** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
|
|
4291
4437
|
this.navObserver = null;
|
|
4292
4438
|
/** Tiempo activo (engagement time, #8). */
|
|
@@ -4338,25 +4484,44 @@ var DeepdotsPopups = class {
|
|
|
4338
4484
|
this.analyticsFeedbackSessionId = id;
|
|
4339
4485
|
}
|
|
4340
4486
|
}) : void 0;
|
|
4487
|
+
const device = config.device ?? collectDeviceInfo(config.appVersion);
|
|
4341
4488
|
this.analytics = new AnalyticsManager({
|
|
4342
4489
|
sink: analyticsSink,
|
|
4343
4490
|
publicKey: config.analytics?.publicKey ?? this.config.apiKey,
|
|
4344
4491
|
language: typeof navigator !== "undefined" ? navigator.language : void 0,
|
|
4345
4492
|
platform: config.platform ?? "web",
|
|
4346
|
-
device
|
|
4493
|
+
device,
|
|
4347
4494
|
maxBatchSize: ANALYTICS_MAX_BATCH_SIZE,
|
|
4348
4495
|
onFlushNeeded: () => this.flushAnalytics()
|
|
4349
4496
|
});
|
|
4497
|
+
this.crashReporter = new CrashReporter({
|
|
4498
|
+
storage,
|
|
4499
|
+
emit: (params) => this.track("deepdots_app_crash", params),
|
|
4500
|
+
device: () => ({ appVersion: device.app_version, osVersion: device.os_version, deviceModel: device.device_model }),
|
|
4501
|
+
sessionId: () => this.tracking?.getSessionId() ?? null,
|
|
4502
|
+
now: () => Date.now(),
|
|
4503
|
+
enabled: () => this.tracking?.isTrackingEnabled() ?? false
|
|
4504
|
+
});
|
|
4505
|
+
if (this.tracking?.isTrackingEnabled()) {
|
|
4506
|
+
this.crashReporter.install();
|
|
4507
|
+
}
|
|
4350
4508
|
collectGeoInfo().then((geo) => {
|
|
4351
4509
|
if (geo) this.analytics?.updateDevice(geo);
|
|
4352
4510
|
}).catch(() => {
|
|
4353
4511
|
});
|
|
4354
4512
|
this.navObserver = new NavigationObserver();
|
|
4355
|
-
this.navObserver.onVisit((v) => this.track("
|
|
4513
|
+
this.navObserver.onVisit((v) => this.track("deepdots_page_view", { screen: v.screen, duration_seconds: v.durationSeconds }));
|
|
4356
4514
|
this.navObserver.install();
|
|
4357
4515
|
this.engagement = new EngagementTracker();
|
|
4358
4516
|
this.engagement.resume();
|
|
4359
4517
|
this.setupAnalyticsFlush();
|
|
4518
|
+
this.track("deepdots_session_start", {});
|
|
4519
|
+
const pendingCrashes = this.crashReporter.drainPendingCrashes();
|
|
4520
|
+
if (this.tracking?.isTrackingEnabled()) {
|
|
4521
|
+
for (const rec of pendingCrashes) {
|
|
4522
|
+
this.track("deepdots_app_crash", crashRecordToParams(rec));
|
|
4523
|
+
}
|
|
4524
|
+
}
|
|
4360
4525
|
this.initialized = true;
|
|
4361
4526
|
this.log("SDK initialized", this.config);
|
|
4362
4527
|
if (this.renderer.init) this.renderer.init();
|
|
@@ -4397,6 +4562,16 @@ var DeepdotsPopups = class {
|
|
|
4397
4562
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
4398
4563
|
this.analytics?.setUserAttributes(attributes);
|
|
4399
4564
|
}
|
|
4565
|
+
/** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
|
|
4566
|
+
reportError(error, options) {
|
|
4567
|
+
if (!this.tracking?.isTrackingEnabled()) return;
|
|
4568
|
+
this.crashReporter?.reportError(error, options);
|
|
4569
|
+
}
|
|
4570
|
+
/** Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. No-op si tracking off. */
|
|
4571
|
+
installReactNativeCrashHandler(errorUtils) {
|
|
4572
|
+
if (!this.tracking?.isTrackingEnabled()) return;
|
|
4573
|
+
this.crashReporter?.installReactNative(errorUtils);
|
|
4574
|
+
}
|
|
4400
4575
|
/**
|
|
4401
4576
|
* Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
|
|
4402
4577
|
* Contact del backend para segmentación/targeting de popups. Identificada por el `userId` del
|
|
@@ -4414,23 +4589,23 @@ var DeepdotsPopups = class {
|
|
|
4414
4589
|
this.analytics?.enterMiniService(name, entryPointType);
|
|
4415
4590
|
this.log("analytics \xB7 enterMiniService:", name, entryPointType ?? "");
|
|
4416
4591
|
}
|
|
4417
|
-
/** Cierra el mini-service
|
|
4418
|
-
exitMiniService() {
|
|
4592
|
+
/** Cierra el mini-service `name` (emite `mini_service_exit` con duración, #27). No-op si tracking off o si ese no está activo. */
|
|
4593
|
+
exitMiniService(name) {
|
|
4419
4594
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
4420
|
-
this.analytics?.exitMiniService();
|
|
4421
|
-
this.log("analytics \xB7 exitMiniService");
|
|
4595
|
+
this.analytics?.exitMiniService(name);
|
|
4596
|
+
this.log("analytics \xB7 exitMiniService:", name);
|
|
4422
4597
|
}
|
|
4423
4598
|
/** Findability (#31/#35): registra una búsqueda. `has_results` se deriva de `resultsCount`. */
|
|
4424
4599
|
trackSearch(query, resultsCount, params) {
|
|
4425
|
-
this.track("
|
|
4600
|
+
this.track("deepdots_search", { query, results_count: resultsCount, has_results: resultsCount > 0, ...params ?? {} });
|
|
4426
4601
|
}
|
|
4427
4602
|
/** Findability friction (#34/#35): señal de fricción con su `friction_topic`. */
|
|
4428
4603
|
trackFindabilityFriction(frictionTopic, params) {
|
|
4429
|
-
this.track("
|
|
4604
|
+
this.track("deepdots_findability_friction", { friction_topic: frictionTopic, ...params ?? {} });
|
|
4430
4605
|
}
|
|
4431
4606
|
/** Funnel: un paso del embudo, correlacionado por `taskId`. El backend reconstruye conversión/drop-off/tiempo. */
|
|
4432
4607
|
trackFunnelStep(funnel, step, taskId, params) {
|
|
4433
|
-
this.track("
|
|
4608
|
+
this.track("deepdots_funnel_step", { funnel, step, task_id: taskId, ...params ?? {} });
|
|
4434
4609
|
}
|
|
4435
4610
|
/**
|
|
4436
4611
|
* Navegación MANUAL (entornos sin History API, p. ej. React Native con React Navigation).
|
|
@@ -4456,7 +4631,7 @@ var DeepdotsPopups = class {
|
|
|
4456
4631
|
onBackground() {
|
|
4457
4632
|
this.navObserver?.stop();
|
|
4458
4633
|
this.navStarted = false;
|
|
4459
|
-
this.
|
|
4634
|
+
if (this.tracking?.isTrackingEnabled()) this.analytics?.exitAllMiniServices();
|
|
4460
4635
|
this.flushEngagement();
|
|
4461
4636
|
this.engagement?.pause();
|
|
4462
4637
|
this.flushAnalytics();
|
|
@@ -4479,7 +4654,7 @@ var DeepdotsPopups = class {
|
|
|
4479
4654
|
flushEngagement() {
|
|
4480
4655
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
4481
4656
|
const ms = this.engagement?.consume() ?? 0;
|
|
4482
|
-
if (ms > 0) this.track("
|
|
4657
|
+
if (ms > 0) this.track("deepdots_user_engagement", { engagement_time_msec: ms });
|
|
4483
4658
|
}
|
|
4484
4659
|
analyticsIdentity() {
|
|
4485
4660
|
return {
|
|
@@ -4494,7 +4669,7 @@ var DeepdotsPopups = class {
|
|
|
4494
4669
|
window.addEventListener("pagehide", () => {
|
|
4495
4670
|
clearInterval(this.analyticsFlushTimer);
|
|
4496
4671
|
this.navObserver?.stop();
|
|
4497
|
-
this.
|
|
4672
|
+
if (this.tracking?.isTrackingEnabled()) this.analytics?.exitAllMiniServices();
|
|
4498
4673
|
this.flushEngagement();
|
|
4499
4674
|
this.flushAnalytics();
|
|
4500
4675
|
});
|
|
@@ -5163,6 +5338,10 @@ function setupReactNative(sdk, config, deps = {}) {
|
|
|
5163
5338
|
const device = deps.deviceInfo ? collectRnDevice(deps.deviceInfo) : config.device;
|
|
5164
5339
|
const platform = deps.platform ?? config.platform;
|
|
5165
5340
|
sdk.init({ ...config, storage, device, platform });
|
|
5341
|
+
const errorUtils = deps.errorUtils ?? globalThis.ErrorUtils;
|
|
5342
|
+
if (errorUtils && typeof errorUtils.setGlobalHandler === "function") {
|
|
5343
|
+
sdk.installReactNativeCrashHandler(errorUtils);
|
|
5344
|
+
}
|
|
5166
5345
|
let subscription;
|
|
5167
5346
|
if (deps.appState?.addEventListener) {
|
|
5168
5347
|
subscription = deps.appState.addEventListener("change", (state) => {
|