@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.mjs
CHANGED
|
@@ -387,8 +387,8 @@ var AnalyticsManager = class {
|
|
|
387
387
|
constructor(options = {}) {
|
|
388
388
|
this.events = [];
|
|
389
389
|
this.attributes = {};
|
|
390
|
-
|
|
391
|
-
this.
|
|
390
|
+
/** Mini-services activos: nombre → timestamp de entrada. El orden de inserción marca el "más reciente". */
|
|
391
|
+
this.activeMiniServices = /* @__PURE__ */ new Map();
|
|
392
392
|
this.sink = options.sink ?? dryRunSink;
|
|
393
393
|
this.now = options.now ?? (() => Date.now());
|
|
394
394
|
this.publicKey = options.publicKey;
|
|
@@ -409,24 +409,38 @@ var AnalyticsManager = class {
|
|
|
409
409
|
this.attributes[k] = String(v);
|
|
410
410
|
}
|
|
411
411
|
}
|
|
412
|
-
/**
|
|
412
|
+
/**
|
|
413
|
+
* Marca el inicio de un mini-service; etiqueta los eventos siguientes con `mini_service`.
|
|
414
|
+
* Admite varios activos a la vez (concurrencia); el "actual" para etiquetar es el más reciente.
|
|
415
|
+
* Reentrar con un nombre ya activo refresca su orden y su tiempo de entrada.
|
|
416
|
+
*/
|
|
413
417
|
enterMiniService(name, entryPointType) {
|
|
414
|
-
this.
|
|
415
|
-
this.
|
|
416
|
-
this.track("
|
|
417
|
-
}
|
|
418
|
-
/**
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
418
|
+
this.activeMiniServices.delete(name);
|
|
419
|
+
this.activeMiniServices.set(name, this.now());
|
|
420
|
+
this.track("deepdots_mini_service_enter", { entry_point_type: entryPointType ?? null });
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Cierra el mini-service `name` emitiendo `mini_service_exit` con su duración (#27).
|
|
424
|
+
* No-op si ese nombre no está activo. Cierre coherente con concurrencia.
|
|
425
|
+
*/
|
|
426
|
+
exitMiniService(name) {
|
|
427
|
+
const enteredAt = this.activeMiniServices.get(name);
|
|
428
|
+
if (enteredAt === void 0) return;
|
|
429
|
+
const durationSeconds = Math.max(0, Math.round((this.now() - enteredAt) / 1e3));
|
|
430
|
+
this.activeMiniServices.delete(name);
|
|
431
|
+
this.track("deepdots_mini_service_exit", { mini_service: name, duration_seconds: durationSeconds });
|
|
432
|
+
}
|
|
433
|
+
/** Cierra TODOS los mini-services activos (orden LIFO). Para el cierre por lifecycle (background/cierre). */
|
|
434
|
+
exitAllMiniServices() {
|
|
435
|
+
for (const name of Array.from(this.activeMiniServices.keys()).reverse()) {
|
|
436
|
+
this.exitMiniService(name);
|
|
437
|
+
}
|
|
425
438
|
}
|
|
426
439
|
/** Registra un evento de analítica (modelo GA: nombre + params). */
|
|
427
440
|
track(name, params) {
|
|
441
|
+
const current = this.getMiniService();
|
|
428
442
|
const merged = {
|
|
429
|
-
...
|
|
443
|
+
...current ? { mini_service: current } : {},
|
|
430
444
|
...params ?? {}
|
|
431
445
|
};
|
|
432
446
|
this.events.push({
|
|
@@ -438,9 +452,11 @@ var AnalyticsManager = class {
|
|
|
438
452
|
this.onFlushNeeded?.();
|
|
439
453
|
}
|
|
440
454
|
}
|
|
441
|
-
/** Mini-service activo
|
|
455
|
+
/** Mini-service actual (el más reciente aún activo) para etiquetar eventos + metadata del survey (#33). */
|
|
442
456
|
getMiniService() {
|
|
443
|
-
|
|
457
|
+
let last = null;
|
|
458
|
+
for (const name of this.activeMiniServices.keys()) last = name;
|
|
459
|
+
return last;
|
|
444
460
|
}
|
|
445
461
|
/** Nº de eventos pendientes de flush. */
|
|
446
462
|
pending() {
|
|
@@ -779,6 +795,132 @@ var ContactManager = class {
|
|
|
779
795
|
}
|
|
780
796
|
};
|
|
781
797
|
|
|
798
|
+
// src/analytics/crash-reporter.ts
|
|
799
|
+
var STACK_MAX = 8e3;
|
|
800
|
+
var QUEUE_KEY = "deepdots.crash.queue";
|
|
801
|
+
var MAX_QUEUED = 20;
|
|
802
|
+
function crashRecordToParams(r) {
|
|
803
|
+
const params = {
|
|
804
|
+
crashed_at: r.crashedAt,
|
|
805
|
+
crash_type: r.crashType,
|
|
806
|
+
message: r.message,
|
|
807
|
+
stack: r.stack,
|
|
808
|
+
fatal: r.fatal,
|
|
809
|
+
handled: r.handled,
|
|
810
|
+
severity: r.severity
|
|
811
|
+
};
|
|
812
|
+
if (r.sessionId) params.crashed_session_id = r.sessionId;
|
|
813
|
+
if (r.appVersion) params.crashed_app_version = r.appVersion;
|
|
814
|
+
if (r.osVersion) params.crashed_os_version = r.osVersion;
|
|
815
|
+
if (r.deviceModel) params.crashed_device_model = r.deviceModel;
|
|
816
|
+
if (r.context) {
|
|
817
|
+
for (const [k, v] of Object.entries(r.context)) params[`ctx_${k}`] = v;
|
|
818
|
+
}
|
|
819
|
+
return params;
|
|
820
|
+
}
|
|
821
|
+
var CrashReporter = class {
|
|
822
|
+
constructor(options) {
|
|
823
|
+
this.options = options;
|
|
824
|
+
this.now = options.now ?? (() => Date.now());
|
|
825
|
+
}
|
|
826
|
+
isEnabled() {
|
|
827
|
+
return this.options.enabled ? this.options.enabled() : true;
|
|
828
|
+
}
|
|
829
|
+
/** Construye un CrashRecord capturando contexto en el momento del crash. */
|
|
830
|
+
buildRecord(error, severity, handled, fatal, context) {
|
|
831
|
+
const err = error;
|
|
832
|
+
const isErr = error instanceof Error;
|
|
833
|
+
const dev = this.options.device();
|
|
834
|
+
const ctx = context ? Object.fromEntries(Object.entries(context).map(([k, v]) => [k, String(v)])) : void 0;
|
|
835
|
+
return {
|
|
836
|
+
crashedAt: this.now(),
|
|
837
|
+
crashType: isErr && err.name || "Error",
|
|
838
|
+
message: isErr ? String(err.message ?? "") : String(error),
|
|
839
|
+
stack: (isErr && typeof err.stack === "string" ? err.stack : "").slice(0, STACK_MAX),
|
|
840
|
+
fatal,
|
|
841
|
+
handled,
|
|
842
|
+
severity,
|
|
843
|
+
sessionId: this.options.sessionId(),
|
|
844
|
+
appVersion: dev.appVersion,
|
|
845
|
+
osVersion: dev.osVersion,
|
|
846
|
+
deviceModel: dev.deviceModel,
|
|
847
|
+
context: ctx
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
/** API pública del host: reporta un error (app viva) → emite el evento ya. */
|
|
851
|
+
reportError(error, options = {}) {
|
|
852
|
+
if (!this.isEnabled()) return;
|
|
853
|
+
const severity = options.severity ?? "error";
|
|
854
|
+
const handled = options.handled ?? true;
|
|
855
|
+
const fatal = severity === "fatal";
|
|
856
|
+
const record = this.buildRecord(error, severity, handled, fatal, options.context);
|
|
857
|
+
this.options.emit(crashRecordToParams(record));
|
|
858
|
+
}
|
|
859
|
+
/** Captura un error no manejado (fatal) → persiste a disco para replay en el siguiente arranque. */
|
|
860
|
+
captureUnhandled(error) {
|
|
861
|
+
this.persist(this.buildRecord(error, "fatal", false, true));
|
|
862
|
+
}
|
|
863
|
+
/** Persiste un crash no capturado a disco para reenviarlo en el siguiente arranque. */
|
|
864
|
+
persist(record) {
|
|
865
|
+
let queue = [];
|
|
866
|
+
try {
|
|
867
|
+
const raw = this.options.storage.getItem(QUEUE_KEY);
|
|
868
|
+
if (raw) queue = JSON.parse(raw);
|
|
869
|
+
if (!Array.isArray(queue)) queue = [];
|
|
870
|
+
} catch {
|
|
871
|
+
queue = [];
|
|
872
|
+
}
|
|
873
|
+
queue.push(record);
|
|
874
|
+
if (queue.length > MAX_QUEUED) queue = queue.slice(queue.length - MAX_QUEUED);
|
|
875
|
+
try {
|
|
876
|
+
this.options.storage.setItem(QUEUE_KEY, JSON.stringify(queue));
|
|
877
|
+
} catch {
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
/** Lee y vacía la cola de crashes pendientes (llamado en init para el replay). */
|
|
881
|
+
drainPendingCrashes() {
|
|
882
|
+
let queue = [];
|
|
883
|
+
try {
|
|
884
|
+
const raw = this.options.storage.getItem(QUEUE_KEY);
|
|
885
|
+
if (raw) queue = JSON.parse(raw);
|
|
886
|
+
if (!Array.isArray(queue)) queue = [];
|
|
887
|
+
} catch {
|
|
888
|
+
queue = [];
|
|
889
|
+
}
|
|
890
|
+
if (queue.length) {
|
|
891
|
+
try {
|
|
892
|
+
this.options.storage.removeItem(QUEUE_KEY);
|
|
893
|
+
} catch {
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return queue;
|
|
897
|
+
}
|
|
898
|
+
/** Instala los handlers globales de errores no capturados (no-op sin `window`). */
|
|
899
|
+
install() {
|
|
900
|
+
if (typeof window === "undefined") return;
|
|
901
|
+
window.addEventListener("error", (e) => {
|
|
902
|
+
if (!this.isEnabled()) return;
|
|
903
|
+
this.captureUnhandled(e.error ?? e.message ?? "error");
|
|
904
|
+
});
|
|
905
|
+
window.addEventListener("unhandledrejection", (e) => {
|
|
906
|
+
if (!this.isEnabled()) return;
|
|
907
|
+
this.captureUnhandled(e.reason ?? "unhandledrejection");
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
/** Engancha `global.ErrorUtils` de RN (no hay `window`): persiste y encadena al handler previo. */
|
|
911
|
+
installReactNative(errorUtils) {
|
|
912
|
+
const previous = errorUtils.getGlobalHandler?.();
|
|
913
|
+
errorUtils.setGlobalHandler((error, isFatal) => {
|
|
914
|
+
if (this.isEnabled()) this.captureUnhandled(error);
|
|
915
|
+
previous?.(error, isFatal);
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
/** @internal Test seam: ejercita el camino de persistencia (window handlers no son testeables en vitest). */
|
|
919
|
+
_persistForTest(error) {
|
|
920
|
+
this.captureUnhandled(error);
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
|
|
782
924
|
// src/core/deepdots-popups.ts
|
|
783
925
|
var EXIT_QUEUE_STORAGE_KEY = "__deepdots_exit_popup_queue__";
|
|
784
926
|
var DAY_IN_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -813,6 +955,8 @@ var DeepdotsPopups = class {
|
|
|
813
955
|
this.analyticsFeedbackSessionId = void 0;
|
|
814
956
|
/** Timer del flush periódico de analytics (cada ANALYTICS_FLUSH_INTERVAL_MS). */
|
|
815
957
|
this.analyticsFlushTimer = void 0;
|
|
958
|
+
/** Crash & error reporting (#14–17). Null hasta init(). */
|
|
959
|
+
this.crashReporter = null;
|
|
816
960
|
/** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
|
|
817
961
|
this.navObserver = null;
|
|
818
962
|
/** Tiempo activo (engagement time, #8). */
|
|
@@ -864,25 +1008,44 @@ var DeepdotsPopups = class {
|
|
|
864
1008
|
this.analyticsFeedbackSessionId = id;
|
|
865
1009
|
}
|
|
866
1010
|
}) : void 0;
|
|
1011
|
+
const device = config.device ?? collectDeviceInfo(config.appVersion);
|
|
867
1012
|
this.analytics = new AnalyticsManager({
|
|
868
1013
|
sink: analyticsSink,
|
|
869
1014
|
publicKey: config.analytics?.publicKey ?? this.config.apiKey,
|
|
870
1015
|
language: typeof navigator !== "undefined" ? navigator.language : void 0,
|
|
871
1016
|
platform: config.platform ?? "web",
|
|
872
|
-
device
|
|
1017
|
+
device,
|
|
873
1018
|
maxBatchSize: ANALYTICS_MAX_BATCH_SIZE,
|
|
874
1019
|
onFlushNeeded: () => this.flushAnalytics()
|
|
875
1020
|
});
|
|
1021
|
+
this.crashReporter = new CrashReporter({
|
|
1022
|
+
storage,
|
|
1023
|
+
emit: (params) => this.track("deepdots_app_crash", params),
|
|
1024
|
+
device: () => ({ appVersion: device.app_version, osVersion: device.os_version, deviceModel: device.device_model }),
|
|
1025
|
+
sessionId: () => this.tracking?.getSessionId() ?? null,
|
|
1026
|
+
now: () => Date.now(),
|
|
1027
|
+
enabled: () => this.tracking?.isTrackingEnabled() ?? false
|
|
1028
|
+
});
|
|
1029
|
+
if (this.tracking?.isTrackingEnabled()) {
|
|
1030
|
+
this.crashReporter.install();
|
|
1031
|
+
}
|
|
876
1032
|
collectGeoInfo().then((geo) => {
|
|
877
1033
|
if (geo) this.analytics?.updateDevice(geo);
|
|
878
1034
|
}).catch(() => {
|
|
879
1035
|
});
|
|
880
1036
|
this.navObserver = new NavigationObserver();
|
|
881
|
-
this.navObserver.onVisit((v) => this.track("
|
|
1037
|
+
this.navObserver.onVisit((v) => this.track("deepdots_page_view", { screen: v.screen, duration_seconds: v.durationSeconds }));
|
|
882
1038
|
this.navObserver.install();
|
|
883
1039
|
this.engagement = new EngagementTracker();
|
|
884
1040
|
this.engagement.resume();
|
|
885
1041
|
this.setupAnalyticsFlush();
|
|
1042
|
+
this.track("deepdots_session_start", {});
|
|
1043
|
+
const pendingCrashes = this.crashReporter.drainPendingCrashes();
|
|
1044
|
+
if (this.tracking?.isTrackingEnabled()) {
|
|
1045
|
+
for (const rec of pendingCrashes) {
|
|
1046
|
+
this.track("deepdots_app_crash", crashRecordToParams(rec));
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
886
1049
|
this.initialized = true;
|
|
887
1050
|
this.log("SDK initialized", this.config);
|
|
888
1051
|
if (this.renderer.init) this.renderer.init();
|
|
@@ -923,6 +1086,16 @@ var DeepdotsPopups = class {
|
|
|
923
1086
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
924
1087
|
this.analytics?.setUserAttributes(attributes);
|
|
925
1088
|
}
|
|
1089
|
+
/** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
|
|
1090
|
+
reportError(error, options) {
|
|
1091
|
+
if (!this.tracking?.isTrackingEnabled()) return;
|
|
1092
|
+
this.crashReporter?.reportError(error, options);
|
|
1093
|
+
}
|
|
1094
|
+
/** Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. No-op si tracking off. */
|
|
1095
|
+
installReactNativeCrashHandler(errorUtils) {
|
|
1096
|
+
if (!this.tracking?.isTrackingEnabled()) return;
|
|
1097
|
+
this.crashReporter?.installReactNative(errorUtils);
|
|
1098
|
+
}
|
|
926
1099
|
/**
|
|
927
1100
|
* Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
|
|
928
1101
|
* Contact del backend para segmentación/targeting de popups. Identificada por el `userId` del
|
|
@@ -940,23 +1113,23 @@ var DeepdotsPopups = class {
|
|
|
940
1113
|
this.analytics?.enterMiniService(name, entryPointType);
|
|
941
1114
|
this.log("analytics \xB7 enterMiniService:", name, entryPointType ?? "");
|
|
942
1115
|
}
|
|
943
|
-
/** Cierra el mini-service
|
|
944
|
-
exitMiniService() {
|
|
1116
|
+
/** Cierra el mini-service `name` (emite `mini_service_exit` con duración, #27). No-op si tracking off o si ese no está activo. */
|
|
1117
|
+
exitMiniService(name) {
|
|
945
1118
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
946
|
-
this.analytics?.exitMiniService();
|
|
947
|
-
this.log("analytics \xB7 exitMiniService");
|
|
1119
|
+
this.analytics?.exitMiniService(name);
|
|
1120
|
+
this.log("analytics \xB7 exitMiniService:", name);
|
|
948
1121
|
}
|
|
949
1122
|
/** Findability (#31/#35): registra una búsqueda. `has_results` se deriva de `resultsCount`. */
|
|
950
1123
|
trackSearch(query, resultsCount, params) {
|
|
951
|
-
this.track("
|
|
1124
|
+
this.track("deepdots_search", { query, results_count: resultsCount, has_results: resultsCount > 0, ...params ?? {} });
|
|
952
1125
|
}
|
|
953
1126
|
/** Findability friction (#34/#35): señal de fricción con su `friction_topic`. */
|
|
954
1127
|
trackFindabilityFriction(frictionTopic, params) {
|
|
955
|
-
this.track("
|
|
1128
|
+
this.track("deepdots_findability_friction", { friction_topic: frictionTopic, ...params ?? {} });
|
|
956
1129
|
}
|
|
957
1130
|
/** Funnel: un paso del embudo, correlacionado por `taskId`. El backend reconstruye conversión/drop-off/tiempo. */
|
|
958
1131
|
trackFunnelStep(funnel, step, taskId, params) {
|
|
959
|
-
this.track("
|
|
1132
|
+
this.track("deepdots_funnel_step", { funnel, step, task_id: taskId, ...params ?? {} });
|
|
960
1133
|
}
|
|
961
1134
|
/**
|
|
962
1135
|
* Navegación MANUAL (entornos sin History API, p. ej. React Native con React Navigation).
|
|
@@ -982,7 +1155,7 @@ var DeepdotsPopups = class {
|
|
|
982
1155
|
onBackground() {
|
|
983
1156
|
this.navObserver?.stop();
|
|
984
1157
|
this.navStarted = false;
|
|
985
|
-
this.
|
|
1158
|
+
if (this.tracking?.isTrackingEnabled()) this.analytics?.exitAllMiniServices();
|
|
986
1159
|
this.flushEngagement();
|
|
987
1160
|
this.engagement?.pause();
|
|
988
1161
|
this.flushAnalytics();
|
|
@@ -1005,7 +1178,7 @@ var DeepdotsPopups = class {
|
|
|
1005
1178
|
flushEngagement() {
|
|
1006
1179
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
1007
1180
|
const ms = this.engagement?.consume() ?? 0;
|
|
1008
|
-
if (ms > 0) this.track("
|
|
1181
|
+
if (ms > 0) this.track("deepdots_user_engagement", { engagement_time_msec: ms });
|
|
1009
1182
|
}
|
|
1010
1183
|
analyticsIdentity() {
|
|
1011
1184
|
return {
|
|
@@ -1020,7 +1193,7 @@ var DeepdotsPopups = class {
|
|
|
1020
1193
|
window.addEventListener("pagehide", () => {
|
|
1021
1194
|
clearInterval(this.analyticsFlushTimer);
|
|
1022
1195
|
this.navObserver?.stop();
|
|
1023
|
-
this.
|
|
1196
|
+
if (this.tracking?.isTrackingEnabled()) this.analytics?.exitAllMiniServices();
|
|
1024
1197
|
this.flushEngagement();
|
|
1025
1198
|
this.flushAnalytics();
|
|
1026
1199
|
});
|
|
@@ -1686,6 +1859,10 @@ function setupReactNative(sdk, config, deps = {}) {
|
|
|
1686
1859
|
const device = deps.deviceInfo ? collectRnDevice(deps.deviceInfo) : config.device;
|
|
1687
1860
|
const platform = deps.platform ?? config.platform;
|
|
1688
1861
|
sdk.init({ ...config, storage, device, platform });
|
|
1862
|
+
const errorUtils = deps.errorUtils ?? globalThis.ErrorUtils;
|
|
1863
|
+
if (errorUtils && typeof errorUtils.setGlobalHandler === "function") {
|
|
1864
|
+
sdk.installReactNativeCrashHandler(errorUtils);
|
|
1865
|
+
}
|
|
1689
1866
|
let subscription;
|
|
1690
1867
|
if (deps.appState?.addEventListener) {
|
|
1691
1868
|
subscription = deps.appState.addEventListener("change", (state) => {
|
|
@@ -1700,6 +1877,7 @@ function setupReactNative(sdk, config, deps = {}) {
|
|
|
1700
1877
|
export {
|
|
1701
1878
|
BrowserPopupRenderer,
|
|
1702
1879
|
ContactManager,
|
|
1880
|
+
CrashReporter,
|
|
1703
1881
|
DeepdotsPopups,
|
|
1704
1882
|
InMemoryStorage,
|
|
1705
1883
|
NoopPopupRenderer,
|
|
@@ -1708,6 +1886,7 @@ export {
|
|
|
1708
1886
|
buildAnalyticsFeedbackBody,
|
|
1709
1887
|
buildSurveyHtml,
|
|
1710
1888
|
collectRnDevice,
|
|
1889
|
+
crashRecordToParams,
|
|
1711
1890
|
createDefaultRenderer,
|
|
1712
1891
|
createFeedbackSink,
|
|
1713
1892
|
mmkvStorage,
|