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