@magicfeedback/popup-sdk 1.1.7 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/index.d.mts +169 -19
- package/dist/index.d.ts +169 -19
- package/dist/index.js +312 -48
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +308 -50
- package/dist/index.mjs.map +1 -1
- package/dist/react-native.d.mts +77 -12
- package/dist/react-native.d.ts +77 -12
- package/dist/react-native.js +309 -48
- package/dist/react-native.js.map +1 -1
- package/dist/react-native.mjs +305 -50
- package/dist/react-native.mjs.map +1 -1
- package/dist/{renderPopup-PS3NJDUR.mjs → renderPopup-NHWJ7YSI.mjs} +7 -1
- package/dist/renderPopup-NHWJ7YSI.mjs.map +1 -0
- package/dist/{renderPopup-EZKDMLVZ.mjs → renderPopup-R53C7DB3.mjs} +7 -1
- package/dist/renderPopup-R53C7DB3.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/renderPopup-EZKDMLVZ.mjs.map +0 -1
- package/dist/renderPopup-PS3NJDUR.mjs.map +0 -1
package/dist/react-native.js
CHANGED
|
@@ -1149,6 +1149,11 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
|
|
|
1149
1149
|
const isDark = style?.theme === "dark";
|
|
1150
1150
|
const theme = {
|
|
1151
1151
|
popupBg: isDark ? "#1e1e1e" : "#fff",
|
|
1152
|
+
// El popup fija su propio fondo, así que debe fijar también el esquema de color.
|
|
1153
|
+
// Sin esto los controles de formulario (input/textarea/select) caen al estilo por
|
|
1154
|
+
// defecto del navegador: en un host con `prefers-color-scheme: dark` se pintan
|
|
1155
|
+
// oscuros con texto blanco sobre el fondo claro del popup.
|
|
1156
|
+
colorScheme: isDark ? "dark" : "light",
|
|
1152
1157
|
textPrimary: isDark ? "#f0f0f0" : "#111",
|
|
1153
1158
|
closeBtnHoverBg: isDark ? "rgba(255,255,255,0.10)" : "rgba(0,0,0,0.06)",
|
|
1154
1159
|
closeBtnHoverColor: isDark ? "#fff" : "#000000"
|
|
@@ -1171,6 +1176,7 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
|
|
|
1171
1176
|
flex-direction: column;
|
|
1172
1177
|
justify-content: flex-start;
|
|
1173
1178
|
background: ${theme.popupBg};
|
|
1179
|
+
color-scheme: ${theme.colorScheme};
|
|
1174
1180
|
border-radius: 8px;
|
|
1175
1181
|
padding: 24px;
|
|
1176
1182
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
@@ -2030,6 +2036,7 @@ var AnalyticsManager = class {
|
|
|
2030
2036
|
this.device = options.device;
|
|
2031
2037
|
this.platform = options.platform ?? "web";
|
|
2032
2038
|
this.maxBatchSize = options.maxBatchSize ?? 20;
|
|
2039
|
+
this.maxBufferedEvents = options.maxBufferedEvents ?? 200;
|
|
2033
2040
|
this.onFlushNeeded = options.onFlushNeeded;
|
|
2034
2041
|
}
|
|
2035
2042
|
/** Actualiza campos de device en runtime (p.ej. country/city tras resolver geo). */
|
|
@@ -2078,6 +2085,15 @@ var AnalyticsManager = class {
|
|
|
2078
2085
|
this.exitMiniService(name);
|
|
2079
2086
|
}
|
|
2080
2087
|
}
|
|
2088
|
+
/**
|
|
2089
|
+
* Olvida lo que pertenecía al usuario anterior (user attributes + métricas) al cambiar de
|
|
2090
|
+
* usuario. NO toca el buffer de eventos: el cierre de sesión ya lo vació con la identidad
|
|
2091
|
+
* vieja, y lo que llegue después es del usuario nuevo.
|
|
2092
|
+
*/
|
|
2093
|
+
resetUserScope() {
|
|
2094
|
+
this.attributes = {};
|
|
2095
|
+
this.metrics = {};
|
|
2096
|
+
}
|
|
2081
2097
|
/** Registra un evento de analítica (modelo GA: nombre + params). */
|
|
2082
2098
|
track(name, params) {
|
|
2083
2099
|
const current = this.getMiniService();
|
|
@@ -2090,6 +2106,7 @@ var AnalyticsManager = class {
|
|
|
2090
2106
|
timestamp: this.now(),
|
|
2091
2107
|
params: Object.keys(merged).length ? merged : void 0
|
|
2092
2108
|
});
|
|
2109
|
+
this.enforceBufferCap();
|
|
2093
2110
|
if (this.events.length >= this.maxBatchSize) {
|
|
2094
2111
|
this.onFlushNeeded?.();
|
|
2095
2112
|
}
|
|
@@ -2120,14 +2137,43 @@ var AnalyticsManager = class {
|
|
|
2120
2137
|
events: [...this.events]
|
|
2121
2138
|
};
|
|
2122
2139
|
}
|
|
2123
|
-
/**
|
|
2124
|
-
|
|
2140
|
+
/**
|
|
2141
|
+
* Envía (vía sink) el lote acumulado y vacía el buffer. No-op si no hay eventos.
|
|
2142
|
+
*
|
|
2143
|
+
* Si el sink falla de forma transitoria (promesa rechazada o excepción síncrona) el lote
|
|
2144
|
+
* se **re-encola** al principio del buffer y se reintenta en el siguiente flush: un error
|
|
2145
|
+
* de red o un 5xx no debe hacer desaparecer los eventos.
|
|
2146
|
+
*/
|
|
2147
|
+
flush(identity, meta) {
|
|
2125
2148
|
if (this.events.length === 0) return null;
|
|
2149
|
+
const batch = this.events;
|
|
2126
2150
|
const payload = this.buildPayload(identity);
|
|
2127
|
-
this.sink(payload);
|
|
2128
2151
|
this.events = [];
|
|
2152
|
+
let result;
|
|
2153
|
+
try {
|
|
2154
|
+
result = meta ? this.sink(payload, meta) : this.sink(payload);
|
|
2155
|
+
} catch {
|
|
2156
|
+
this.requeue(batch);
|
|
2157
|
+
return payload;
|
|
2158
|
+
}
|
|
2159
|
+
if (result && typeof result.then === "function") {
|
|
2160
|
+
result.catch(() => this.requeue(batch));
|
|
2161
|
+
}
|
|
2129
2162
|
return payload;
|
|
2130
2163
|
}
|
|
2164
|
+
/**
|
|
2165
|
+
* Devuelve un lote fallido al buffer, delante de lo que haya llegado mientras se enviaba
|
|
2166
|
+
* (orden cronológico). Con el buffer lleno se descartan los eventos más antiguos.
|
|
2167
|
+
*/
|
|
2168
|
+
requeue(batch) {
|
|
2169
|
+
this.events = [...batch, ...this.events];
|
|
2170
|
+
this.enforceBufferCap();
|
|
2171
|
+
}
|
|
2172
|
+
/** Techo del buffer: si el envío lleva rato fallando, se sacrifica lo más antiguo. */
|
|
2173
|
+
enforceBufferCap() {
|
|
2174
|
+
const overflow = this.events.length - this.maxBufferedEvents;
|
|
2175
|
+
if (overflow > 0) this.events.splice(0, overflow);
|
|
2176
|
+
}
|
|
2131
2177
|
};
|
|
2132
2178
|
|
|
2133
2179
|
// src/analytics/feedback-payload.ts
|
|
@@ -2135,7 +2181,7 @@ function pushKV(list, key, value) {
|
|
|
2135
2181
|
if (value === void 0 || value === null || value === "") return;
|
|
2136
2182
|
list.push({ key, value: [String(value)] });
|
|
2137
2183
|
}
|
|
2138
|
-
function buildAnalyticsFeedbackBody(envelope, keys, feedbackSessionId) {
|
|
2184
|
+
function buildAnalyticsFeedbackBody(envelope, keys, feedbackSessionId, options) {
|
|
2139
2185
|
const { context } = envelope;
|
|
2140
2186
|
const profile = [];
|
|
2141
2187
|
pushKV(profile, "external-user-id", envelope.userId);
|
|
@@ -2186,37 +2232,91 @@ function buildAnalyticsFeedbackBody(envelope, keys, feedbackSessionId) {
|
|
|
2186
2232
|
},
|
|
2187
2233
|
publicKey: keys.publicKey,
|
|
2188
2234
|
integration: keys.integration,
|
|
2189
|
-
|
|
2235
|
+
// Único marcador de cierre acordado con backend: el último lote de la sesión.
|
|
2236
|
+
completed: options?.sessionEnd === true,
|
|
2190
2237
|
...feedbackSessionId ? { sessionId: feedbackSessionId } : {}
|
|
2191
2238
|
};
|
|
2192
2239
|
}
|
|
2240
|
+
var KEEPALIVE_MAX_BYTES = 6e4;
|
|
2241
|
+
function isRetryableStatus(status) {
|
|
2242
|
+
return status >= 500 || status === 408 || status === 429;
|
|
2243
|
+
}
|
|
2244
|
+
async function safeBodyText(res) {
|
|
2245
|
+
try {
|
|
2246
|
+
return (await res.text()).slice(0, 500);
|
|
2247
|
+
} catch {
|
|
2248
|
+
return "";
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2193
2251
|
function createFeedbackSink(options) {
|
|
2194
2252
|
let feedbackSessionId;
|
|
2195
|
-
|
|
2196
|
-
|
|
2253
|
+
let firstPostInFlight = null;
|
|
2254
|
+
const url = `${options.baseUrl}/sdk/feedback`;
|
|
2255
|
+
const post = async (body, final, sessionEnd = false) => {
|
|
2256
|
+
const json = JSON.stringify(body);
|
|
2257
|
+
const small = json.length <= KEEPALIVE_MAX_BYTES;
|
|
2258
|
+
if (final && options.sendBeaconImpl && small) {
|
|
2259
|
+
const payload = typeof Blob !== "undefined" ? new Blob([json], { type: "application/json" }) : json;
|
|
2260
|
+
if (options.sendBeaconImpl(url, payload)) {
|
|
2261
|
+
options.log?.("[DeepdotsAnalytics] flush final v\xEDa sendBeacon");
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
options.log?.("[DeepdotsAnalytics] sendBeacon rechaz\xF3 el lote; fallback a fetch");
|
|
2265
|
+
}
|
|
2197
2266
|
const f = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
|
|
2198
2267
|
if (!f) {
|
|
2199
2268
|
options.log?.("[DeepdotsAnalytics] no fetch disponible; payload no enviado", body);
|
|
2200
2269
|
return;
|
|
2201
2270
|
}
|
|
2202
|
-
|
|
2203
|
-
f(`${options.baseUrl}/sdk/feedback`, {
|
|
2271
|
+
const res = await f(url, {
|
|
2204
2272
|
method: "POST",
|
|
2205
2273
|
headers: { "Content-Type": "application/json" },
|
|
2206
|
-
body:
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
}
|
|
2217
|
-
|
|
2274
|
+
body: json,
|
|
2275
|
+
// keepalive: el POST sobrevive a la navegación mientras el body sea pequeño.
|
|
2276
|
+
...small ? { keepalive: true } : {}
|
|
2277
|
+
});
|
|
2278
|
+
if (!res.ok) {
|
|
2279
|
+
const detail = await safeBodyText(res);
|
|
2280
|
+
if (isRetryableStatus(res.status)) {
|
|
2281
|
+
throw new Error(`POST /sdk/feedback ${res.status} (reintentable): ${detail}`);
|
|
2282
|
+
}
|
|
2283
|
+
options.log?.(
|
|
2284
|
+
`[DeepdotsAnalytics] POST /sdk/feedback rechazado con ${res.status}; lote DESCARTADO:`,
|
|
2285
|
+
detail
|
|
2286
|
+
);
|
|
2287
|
+
return;
|
|
2288
|
+
}
|
|
2289
|
+
if (sessionEnd) return;
|
|
2290
|
+
try {
|
|
2291
|
+
const data = await res.json();
|
|
2292
|
+
if (data?.sessionId && data.sessionId !== feedbackSessionId) {
|
|
2293
|
+
feedbackSessionId = data.sessionId;
|
|
2294
|
+
options.log?.("[DeepdotsAnalytics] feedbackSessionId cacheado:", feedbackSessionId);
|
|
2295
|
+
options.onSessionId?.(feedbackSessionId);
|
|
2218
2296
|
}
|
|
2219
|
-
}
|
|
2297
|
+
} catch {
|
|
2298
|
+
}
|
|
2299
|
+
};
|
|
2300
|
+
return async (envelope, meta) => {
|
|
2301
|
+
const final = meta?.final === true;
|
|
2302
|
+
const sessionEnd = meta?.sessionEnd === true;
|
|
2303
|
+
if (!feedbackSessionId && firstPostInFlight && !final) {
|
|
2304
|
+
await firstPostInFlight;
|
|
2305
|
+
}
|
|
2306
|
+
const body = buildAnalyticsFeedbackBody(envelope, options.keys, feedbackSessionId, { sessionEnd });
|
|
2307
|
+
options.log?.("[DeepdotsAnalytics] POST /sdk/feedback \u2192", body);
|
|
2308
|
+
const sent = post(body, final, sessionEnd);
|
|
2309
|
+
if (sessionEnd) {
|
|
2310
|
+
feedbackSessionId = void 0;
|
|
2311
|
+
firstPostInFlight = null;
|
|
2312
|
+
options.onSessionReset?.();
|
|
2313
|
+
} else if (!feedbackSessionId) {
|
|
2314
|
+
firstPostInFlight = sent.then(
|
|
2315
|
+
() => void 0,
|
|
2316
|
+
() => void 0
|
|
2317
|
+
);
|
|
2318
|
+
}
|
|
2319
|
+
await sent;
|
|
2220
2320
|
};
|
|
2221
2321
|
}
|
|
2222
2322
|
|
|
@@ -2649,6 +2749,7 @@ var CrashReporter = class {
|
|
|
2649
2749
|
init_logger();
|
|
2650
2750
|
|
|
2651
2751
|
// src/analytics/messaging.ts
|
|
2752
|
+
var MESSAGE_CHANNELS = ["push", "in_app"];
|
|
2652
2753
|
function buildMessageParams(stage, o) {
|
|
2653
2754
|
const p = {
|
|
2654
2755
|
stage,
|
|
@@ -2661,6 +2762,57 @@ function buildMessageParams(stage, o) {
|
|
|
2661
2762
|
if (o.currency) p.currency = o.currency;
|
|
2662
2763
|
return { ...p, ...o.params ?? {} };
|
|
2663
2764
|
}
|
|
2765
|
+
var MAX_TRACKED_MESSAGES = 500;
|
|
2766
|
+
var MessageGuard = class {
|
|
2767
|
+
constructor(maxTrackedMessages = MAX_TRACKED_MESSAGES) {
|
|
2768
|
+
this.maxTrackedMessages = maxTrackedMessages;
|
|
2769
|
+
/** message_id → canal fijado + stages ya emitidos. El orden de inserción marca la eviction. */
|
|
2770
|
+
this.seen = /* @__PURE__ */ new Map();
|
|
2771
|
+
}
|
|
2772
|
+
evaluate(stage, options) {
|
|
2773
|
+
const channel = options.channel;
|
|
2774
|
+
if (!MESSAGE_CHANNELS.includes(channel)) {
|
|
2775
|
+
return {
|
|
2776
|
+
emit: false,
|
|
2777
|
+
reason: "invalid_channel",
|
|
2778
|
+
detail: `channel "${channel}" no v\xE1lido (esperado ${MESSAGE_CHANNELS.join(" | ")})`
|
|
2779
|
+
};
|
|
2780
|
+
}
|
|
2781
|
+
const entry = this.seen.get(options.id);
|
|
2782
|
+
if (!entry) {
|
|
2783
|
+
this.remember(options.id, channel, stage);
|
|
2784
|
+
return { emit: true };
|
|
2785
|
+
}
|
|
2786
|
+
if (entry.channel !== channel) {
|
|
2787
|
+
return {
|
|
2788
|
+
emit: false,
|
|
2789
|
+
reason: "channel_conflict",
|
|
2790
|
+
detail: `message_id "${options.id}" ya se report\xF3 en channel "${entry.channel}"; se descarta "${channel}"`
|
|
2791
|
+
};
|
|
2792
|
+
}
|
|
2793
|
+
if (entry.stages.has(stage)) {
|
|
2794
|
+
return {
|
|
2795
|
+
emit: false,
|
|
2796
|
+
reason: "duplicate_stage",
|
|
2797
|
+
detail: `stage "${stage}" ya emitido para message_id "${options.id}"`
|
|
2798
|
+
};
|
|
2799
|
+
}
|
|
2800
|
+
entry.stages.add(stage);
|
|
2801
|
+
return { emit: true };
|
|
2802
|
+
}
|
|
2803
|
+
/** Olvida todo lo vigilado (nueva sesión). */
|
|
2804
|
+
reset() {
|
|
2805
|
+
this.seen.clear();
|
|
2806
|
+
}
|
|
2807
|
+
remember(id, channel, stage) {
|
|
2808
|
+
this.seen.set(id, { channel, stages: /* @__PURE__ */ new Set([stage]) });
|
|
2809
|
+
while (this.seen.size > this.maxTrackedMessages) {
|
|
2810
|
+
const oldest = this.seen.keys().next();
|
|
2811
|
+
if (oldest.done) break;
|
|
2812
|
+
this.seen.delete(oldest.value);
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
};
|
|
2664
2816
|
|
|
2665
2817
|
// src/core/deepdots-popups.ts
|
|
2666
2818
|
var EXIT_QUEUE_STORAGE_KEY = "__deepdots_exit_popup_queue__";
|
|
@@ -2702,20 +2854,40 @@ var DeepdotsPopups = class {
|
|
|
2702
2854
|
this.analyticsFlushTimer = void 0;
|
|
2703
2855
|
/** Crash & error reporting (#14–17). Null hasta init(). */
|
|
2704
2856
|
this.crashReporter = null;
|
|
2857
|
+
/** Protecciones del funnel de Messaging (#18–22). Vigencia de sesión: se reinicia en init(). */
|
|
2858
|
+
this.messageGuard = new MessageGuard();
|
|
2705
2859
|
/** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
|
|
2706
2860
|
this.navObserver = null;
|
|
2707
2861
|
/** Tiempo activo (engagement time, #8). */
|
|
2708
2862
|
this.engagement = null;
|
|
2709
2863
|
/** Atributos internos del usuario identificado → POST /sdk/popups/contact. Null si no hay userId. */
|
|
2710
2864
|
this.contact = null;
|
|
2865
|
+
/** Storage resuelto en init() (inyectado por el host o localStorage). Reusado al cambiar de usuario. */
|
|
2866
|
+
this.storage = null;
|
|
2711
2867
|
/** Marca si ya se inició la navegación manual (setScreen) — para RN sin History API. */
|
|
2712
2868
|
this.navStarted = false;
|
|
2869
|
+
/**
|
|
2870
|
+
* `true` entre `session_start` y `session_end`. Hace idempotente tanto la apertura como
|
|
2871
|
+
* el cierre (p. ej. `visibilitychange` + `pagehide` seguidos) y permite abrir una sesión
|
|
2872
|
+
* nueva al volver a foreground o al conceder el consentimiento más tarde.
|
|
2873
|
+
*/
|
|
2874
|
+
this.sessionOpen = false;
|
|
2875
|
+
/**
|
|
2876
|
+
* Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
|
|
2877
|
+
* verdad para el context de analytics Y para la segmentación por idioma de los popups.
|
|
2878
|
+
*/
|
|
2879
|
+
this.language = void 0;
|
|
2713
2880
|
}
|
|
2714
2881
|
/** Initialize the SDK with configuration */
|
|
2715
2882
|
init(config) {
|
|
2716
2883
|
this.logger = config.logger ?? console;
|
|
2717
2884
|
setLogger(this.logger);
|
|
2718
2885
|
if (this.initialized) {
|
|
2886
|
+
const nextUserId = config.userId || void 0;
|
|
2887
|
+
if (nextUserId !== this.config?.userId) {
|
|
2888
|
+
this.setUserId(nextUserId);
|
|
2889
|
+
return;
|
|
2890
|
+
}
|
|
2719
2891
|
this.log("SDK already initialized");
|
|
2720
2892
|
return;
|
|
2721
2893
|
}
|
|
@@ -2728,6 +2900,7 @@ var DeepdotsPopups = class {
|
|
|
2728
2900
|
userId: config.userId || void 0
|
|
2729
2901
|
};
|
|
2730
2902
|
const storage = config.storage ?? createDefaultStorage();
|
|
2903
|
+
this.storage = storage;
|
|
2731
2904
|
this.tracking = new TrackingManager({
|
|
2732
2905
|
storage,
|
|
2733
2906
|
clientUserId: this.config.userId,
|
|
@@ -2751,19 +2924,26 @@ var DeepdotsPopups = class {
|
|
|
2751
2924
|
baseUrl: this.baseUrl,
|
|
2752
2925
|
keys: config.analytics,
|
|
2753
2926
|
log: (...a) => this.log(...a),
|
|
2927
|
+
// sendBeacon: único transporte que sobrevive al cierre de la página.
|
|
2928
|
+
sendBeaconImpl: typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" ? (url, body) => navigator.sendBeacon(url, body) : void 0,
|
|
2754
2929
|
onSessionId: (id) => {
|
|
2755
2930
|
this.analyticsFeedbackSessionId = id;
|
|
2931
|
+
},
|
|
2932
|
+
onSessionReset: () => {
|
|
2933
|
+
this.analyticsFeedbackSessionId = void 0;
|
|
2756
2934
|
}
|
|
2757
2935
|
}) : createDryRunSink((...a) => this.logger.log(...a));
|
|
2758
2936
|
const device = config.device ?? collectDeviceInfo(config.appVersion);
|
|
2937
|
+
this.language = resolveLanguage({
|
|
2938
|
+
explicit: config.language,
|
|
2939
|
+
navigator: typeof navigator !== "undefined" ? navigator : void 0,
|
|
2940
|
+
intl: typeof Intl !== "undefined" ? Intl : void 0
|
|
2941
|
+
});
|
|
2942
|
+
this.messageGuard.reset();
|
|
2759
2943
|
this.analytics = new AnalyticsManager({
|
|
2760
2944
|
sink: analyticsSink,
|
|
2761
2945
|
publicKey: config.analytics?.publicKey ?? this.config.apiKey,
|
|
2762
|
-
language:
|
|
2763
|
-
explicit: config.language,
|
|
2764
|
-
navigator: typeof navigator !== "undefined" ? navigator : void 0,
|
|
2765
|
-
intl: typeof Intl !== "undefined" ? Intl : void 0
|
|
2766
|
-
}),
|
|
2946
|
+
language: this.language,
|
|
2767
2947
|
platform: config.platform ?? "web",
|
|
2768
2948
|
device,
|
|
2769
2949
|
maxBatchSize: ANALYTICS_MAX_BATCH_SIZE,
|
|
@@ -2795,7 +2975,7 @@ var DeepdotsPopups = class {
|
|
|
2795
2975
|
this.engagement = new EngagementTracker();
|
|
2796
2976
|
this.engagement.resume();
|
|
2797
2977
|
this.setupAnalyticsFlush();
|
|
2798
|
-
this.
|
|
2978
|
+
this.openSession();
|
|
2799
2979
|
const pendingCrashes = this.crashReporter.drainPendingCrashes();
|
|
2800
2980
|
if (this.tracking?.isTrackingEnabled()) {
|
|
2801
2981
|
for (const rec of pendingCrashes) {
|
|
@@ -2827,7 +3007,9 @@ var DeepdotsPopups = class {
|
|
|
2827
3007
|
}
|
|
2828
3008
|
/** Activa/desactiva el tracking (identidad + sesión + analytics). Kill-switch del contrato §7bis. */
|
|
2829
3009
|
setTrackingEnabled(enabled) {
|
|
3010
|
+
if (!enabled) this.closeSession("tracking_disabled");
|
|
2830
3011
|
this.tracking?.setTrackingEnabled(enabled);
|
|
3012
|
+
if (enabled) this.openSession();
|
|
2831
3013
|
this.log("tracking \xB7 setTrackingEnabled:", enabled, "\xB7 session_id:", this.tracking?.getSessionId() ?? null);
|
|
2832
3014
|
}
|
|
2833
3015
|
// ───────── Analytics (canal separado del feedback, vinculado por user_id) ─────────
|
|
@@ -2897,8 +3079,18 @@ var DeepdotsPopups = class {
|
|
|
2897
3079
|
trackFunnelStep(funnel, step, taskId, params) {
|
|
2898
3080
|
this.track("deepdots_funnel_step", { funnel, step, task_id: taskId, ...params ?? {} });
|
|
2899
3081
|
}
|
|
2900
|
-
/**
|
|
3082
|
+
/**
|
|
3083
|
+
* Messaging (#18–22): registra una etapa del funnel de una notificación (push/in-app).
|
|
3084
|
+
* No-op si tracking off. El evento se descarta (con warning) si el `channel` no es válido,
|
|
3085
|
+
* si ese `(message_id, stage)` ya se emitió en la sesión, o si el `message_id` ya se reportó
|
|
3086
|
+
* en otro canal — ver `MessageGuard`.
|
|
3087
|
+
*/
|
|
2901
3088
|
trackMessage(stage, options) {
|
|
3089
|
+
const verdict = this.messageGuard.evaluate(stage, options);
|
|
3090
|
+
if (!verdict.emit) {
|
|
3091
|
+
console.warn(`[DeepdotsPopups] trackMessage descartado (${verdict.reason}): ${verdict.detail}`);
|
|
3092
|
+
return;
|
|
3093
|
+
}
|
|
2902
3094
|
this.track("deepdots_message", buildMessageParams(stage, options));
|
|
2903
3095
|
}
|
|
2904
3096
|
/**
|
|
@@ -2914,21 +3106,86 @@ var DeepdotsPopups = class {
|
|
|
2914
3106
|
this.navObserver.visit(name);
|
|
2915
3107
|
}
|
|
2916
3108
|
}
|
|
2917
|
-
/** App a foreground (RN: AppState 'active'): reanuda el engagement time. */
|
|
3109
|
+
/** App a foreground (RN: AppState 'active'): reanuda el engagement time y abre sesión nueva. */
|
|
2918
3110
|
onForeground() {
|
|
2919
3111
|
this.engagement?.resume();
|
|
3112
|
+
this.openSession();
|
|
2920
3113
|
}
|
|
2921
3114
|
/**
|
|
2922
|
-
* App a background (RN: AppState 'background'):
|
|
2923
|
-
* mini-
|
|
3115
|
+
* App a background (RN: AppState 'background'): FIN DE SESIÓN. Cierra pantalla actual
|
|
3116
|
+
* (page_view), mini-services (mini_service_exit), emite engagement + `session_end` y
|
|
3117
|
+
* envía el último lote con `completed:true`.
|
|
3118
|
+
*
|
|
3119
|
+
* Es la única señal disponible en móvil: el kill de la app (swipe) NO da callback.
|
|
2924
3120
|
*/
|
|
2925
3121
|
onBackground() {
|
|
3122
|
+
this.closeSession("background");
|
|
3123
|
+
this.engagement?.pause();
|
|
3124
|
+
}
|
|
3125
|
+
/**
|
|
3126
|
+
* Cierre EXPLÍCITO de la sesión por parte del host (logout, fin de flujo…): emite
|
|
3127
|
+
* `session_end` y envía el último lote con `completed:true`.
|
|
3128
|
+
*/
|
|
3129
|
+
endSession() {
|
|
3130
|
+
this.closeSession("manual");
|
|
3131
|
+
}
|
|
3132
|
+
/**
|
|
3133
|
+
* Cambio de usuario (login / logout / cambio de cuenta). Cierra la sesión del usuario
|
|
3134
|
+
* anterior (`session_end` con `reason: 'user_change'` + `completed:true`), cambia la
|
|
3135
|
+
* identidad y abre una sesión nueva. Sin `userId` vuelve al id anónimo del SDK.
|
|
3136
|
+
*
|
|
3137
|
+
* Equivale a llamar a `init()` otra vez con otro `userId`.
|
|
3138
|
+
*/
|
|
3139
|
+
setUserId(userId) {
|
|
3140
|
+
const next = userId || void 0;
|
|
3141
|
+
if (!this.initialized || !this.config || next === this.config.userId) return;
|
|
3142
|
+
this.closeSession("user_change");
|
|
3143
|
+
const enabled = this.tracking?.isTrackingEnabled() ?? true;
|
|
3144
|
+
const storage = this.storage ?? createDefaultStorage();
|
|
3145
|
+
this.config.userId = next;
|
|
3146
|
+
this.tracking = new TrackingManager({ storage, clientUserId: next, enabled });
|
|
3147
|
+
this.contact = next ? new ContactManager({
|
|
3148
|
+
storage,
|
|
3149
|
+
publicKey: this.config.apiKey ?? "",
|
|
3150
|
+
userId: next,
|
|
3151
|
+
post: (body) => this.postContact(body)
|
|
3152
|
+
}) : null;
|
|
3153
|
+
this.analytics?.resetUserScope();
|
|
3154
|
+
this.messageGuard.reset();
|
|
3155
|
+
this.log("tracking \xB7 user_change \xB7 user_id:", this.tracking.getUserId());
|
|
3156
|
+
this.openSession();
|
|
3157
|
+
}
|
|
3158
|
+
/**
|
|
3159
|
+
* Cierra la sesión actual: vuelca todo lo que quedaba abierto y envía el último lote
|
|
3160
|
+
* marcado con `completed:true`, para que el backend cierre el registro. El `sessionId`
|
|
3161
|
+
* cacheado se olvida → el lote siguiente abre un registro nuevo.
|
|
3162
|
+
*
|
|
3163
|
+
* Idempotente: dos cierres seguidos (p. ej. `visibilitychange` + `pagehide`) no duplican
|
|
3164
|
+
* el `session_end`.
|
|
3165
|
+
*/
|
|
3166
|
+
closeSession(reason) {
|
|
3167
|
+
if (!this.sessionOpen) return;
|
|
3168
|
+
if (!this.tracking?.isTrackingEnabled()) return;
|
|
3169
|
+
this.sessionOpen = false;
|
|
2926
3170
|
this.navObserver?.stop();
|
|
2927
3171
|
this.navStarted = false;
|
|
2928
|
-
|
|
3172
|
+
this.analytics?.exitAllMiniServices();
|
|
2929
3173
|
this.flushEngagement();
|
|
2930
|
-
this.
|
|
2931
|
-
this.flushAnalytics();
|
|
3174
|
+
this.track("deepdots_session_end", { reason });
|
|
3175
|
+
this.flushAnalytics({ final: reason === "page_hide", sessionEnd: true });
|
|
3176
|
+
this.analyticsFeedbackSessionId = void 0;
|
|
3177
|
+
this.tracking?.setSessionId(null);
|
|
3178
|
+
this.log("tracking \xB7 session_end:", reason);
|
|
3179
|
+
}
|
|
3180
|
+
/**
|
|
3181
|
+
* Abre sesión (`session_start`) si no hay una abierta y el tracking está activo. Idempotente:
|
|
3182
|
+
* se llama en init(), al volver a foreground y al conceder el consentimiento.
|
|
3183
|
+
*/
|
|
3184
|
+
openSession() {
|
|
3185
|
+
if (this.sessionOpen) return;
|
|
3186
|
+
if (!this.tracking?.isTrackingEnabled()) return;
|
|
3187
|
+
this.sessionOpen = true;
|
|
3188
|
+
this.track("deepdots_session_start", {});
|
|
2932
3189
|
}
|
|
2933
3190
|
/** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
|
|
2934
3191
|
previewAnalytics() {
|
|
@@ -2939,10 +3196,14 @@ var DeepdotsPopups = class {
|
|
|
2939
3196
|
events: []
|
|
2940
3197
|
};
|
|
2941
3198
|
}
|
|
2942
|
-
/**
|
|
2943
|
-
|
|
3199
|
+
/**
|
|
3200
|
+
* Envía el lote acumulado de analytics y vacía el buffer.
|
|
3201
|
+
* `final: true` (cierre de página/app) cambia el transporte a `sendBeacon`, que sobrevive
|
|
3202
|
+
* al unload; un lote que falle por red o 5xx se re-encola para el siguiente flush.
|
|
3203
|
+
*/
|
|
3204
|
+
flushAnalytics(options) {
|
|
2944
3205
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
2945
|
-
this.analytics?.flush(this.analyticsIdentity());
|
|
3206
|
+
this.analytics?.flush(this.analyticsIdentity(), options);
|
|
2946
3207
|
}
|
|
2947
3208
|
/** Emite un evento `user_engagement` con el tiempo activo acumulado (#8). */
|
|
2948
3209
|
flushEngagement() {
|
|
@@ -2962,10 +3223,7 @@ var DeepdotsPopups = class {
|
|
|
2962
3223
|
this.analyticsFlushTimer = setInterval(() => this.flushAnalytics(), ANALYTICS_FLUSH_INTERVAL_MS);
|
|
2963
3224
|
window.addEventListener("pagehide", () => {
|
|
2964
3225
|
clearInterval(this.analyticsFlushTimer);
|
|
2965
|
-
this.
|
|
2966
|
-
if (this.tracking?.isTrackingEnabled()) this.analytics?.exitAllMiniServices();
|
|
2967
|
-
this.flushEngagement();
|
|
2968
|
-
this.flushAnalytics();
|
|
3226
|
+
this.closeSession("page_hide");
|
|
2969
3227
|
});
|
|
2970
3228
|
document.addEventListener("visibilitychange", () => {
|
|
2971
3229
|
if (document.visibilityState === "hidden") {
|
|
@@ -3086,14 +3344,16 @@ var DeepdotsPopups = class {
|
|
|
3086
3344
|
matchesSegmentsLang(def) {
|
|
3087
3345
|
const langs = def.segments?.lang;
|
|
3088
3346
|
if (!langs || langs.length === 0) return true;
|
|
3089
|
-
const
|
|
3090
|
-
if (!
|
|
3091
|
-
this.debug("No
|
|
3347
|
+
const currentLang = (this.language ?? "").toLowerCase();
|
|
3348
|
+
if (!currentLang) {
|
|
3349
|
+
this.debug("No language available for lang comparison", { popupId: def.id, langs });
|
|
3092
3350
|
return true;
|
|
3093
3351
|
}
|
|
3094
|
-
const matches = langs.some(
|
|
3352
|
+
const matches = langs.some(
|
|
3353
|
+
(lang) => typeof lang === "string" && currentLang.startsWith(lang.toLowerCase())
|
|
3354
|
+
);
|
|
3095
3355
|
if (!matches) {
|
|
3096
|
-
this.debug("Lang mismatch for popup", { popupId: def.id, langs,
|
|
3356
|
+
this.debug("Lang mismatch for popup", { popupId: def.id, langs, currentLang });
|
|
3097
3357
|
}
|
|
3098
3358
|
return matches;
|
|
3099
3359
|
}
|
|
@@ -3638,7 +3898,8 @@ function setupReactNative(sdk2, config, deps = {}) {
|
|
|
3638
3898
|
if (deps.appState?.addEventListener) {
|
|
3639
3899
|
subscription = deps.appState.addEventListener("change", (state) => {
|
|
3640
3900
|
if (state === "active") sdk2.onForeground();
|
|
3641
|
-
else sdk2.onBackground();
|
|
3901
|
+
else if (state === "background") sdk2.onBackground();
|
|
3902
|
+
else sdk2.flushAnalytics();
|
|
3642
3903
|
});
|
|
3643
3904
|
}
|
|
3644
3905
|
return () => {
|