@magicfeedback/popup-sdk 1.1.6 → 1.1.8
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 +63 -8
- package/dist/index.d.ts +63 -8
- package/dist/index.js +244 -43
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +242 -41
- package/dist/index.mjs.map +1 -1
- package/dist/react-native.d.mts +30 -3
- package/dist/react-native.d.ts +30 -3
- package/dist/react-native.js +244 -43
- package/dist/react-native.js.map +1 -1
- package/dist/react-native.mjs +242 -41
- package/dist/react-native.mjs.map +1 -1
- package/package.json +1 -1
package/dist/react-native.d.mts
CHANGED
|
@@ -76,6 +76,8 @@ interface AnalyticsEnvelope {
|
|
|
76
76
|
device?: DeviceInfo;
|
|
77
77
|
/** User attributes del cliente, usados para breakdowns (registration_status, pass_type, …). */
|
|
78
78
|
attributes: Record<string, string>;
|
|
79
|
+
/** Métricas del host: valores medibles persistentes → van en `feedback.metrics` del body. */
|
|
80
|
+
metrics?: Record<string, string>;
|
|
79
81
|
};
|
|
80
82
|
events: AnalyticsEvent[];
|
|
81
83
|
}
|
|
@@ -92,7 +94,8 @@ interface AnalyticsEnvelope {
|
|
|
92
94
|
* - cada entrada usa `value: string[]` (array de un elemento)
|
|
93
95
|
* - eventos: {key: nombre_evento, value: [JSON(timestamp + params)]}
|
|
94
96
|
* - identidad (user_id) → `profile` como `external-user-id`
|
|
95
|
-
* -
|
|
97
|
+
* - métricas del host (setMetric) → `feedback.metrics` (mismo shape que metadata, sin prefijo)
|
|
98
|
+
* - `answers` vacío
|
|
96
99
|
* - `text` vacío
|
|
97
100
|
*/
|
|
98
101
|
|
|
@@ -135,6 +138,14 @@ interface DeepdotsInitParams {
|
|
|
135
138
|
storage?: KeyValueStorage;
|
|
136
139
|
/** Platform used in the analytics envelope. Default is 'web'; in RN pass 'android'/'ios'. */
|
|
137
140
|
platform?: 'web' | 'android' | 'ios';
|
|
141
|
+
/**
|
|
142
|
+
* BCP-47 language tag (e.g. 'es-ES') used for the analytics context AND for popup
|
|
143
|
+
* language segmentation (`segments.lang`). If omitted, the SDK auto-detects:
|
|
144
|
+
* `navigator.language` on web, or the `Intl` locale as a fallback (works in
|
|
145
|
+
* React Native with Hermes, where `navigator.language` is absent). If no source
|
|
146
|
+
* resolves a language, popups with `segments.lang` cannot be filtered and are shown.
|
|
147
|
+
*/
|
|
148
|
+
language?: string;
|
|
138
149
|
/** Injectable device info (RN: from react-native-device-info). If missing, it is derived from the browser. */
|
|
139
150
|
device?: DeviceInfo;
|
|
140
151
|
/**
|
|
@@ -324,6 +335,11 @@ declare class DeepdotsPopups {
|
|
|
324
335
|
private contact;
|
|
325
336
|
/** Marca si ya se inició la navegación manual (setScreen) — para RN sin History API. */
|
|
326
337
|
private navStarted;
|
|
338
|
+
/**
|
|
339
|
+
* Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
|
|
340
|
+
* verdad para el context de analytics Y para la segmentación por idioma de los popups.
|
|
341
|
+
*/
|
|
342
|
+
private language;
|
|
327
343
|
/** Initialize the SDK with configuration */
|
|
328
344
|
init(config: DeepdotsInitParams): void;
|
|
329
345
|
/** User id actual (generado por el SDK o provisto por el cliente). Null si tracking off. */
|
|
@@ -336,6 +352,11 @@ declare class DeepdotsPopups {
|
|
|
336
352
|
track(name: string, params?: Record<string, unknown>): void;
|
|
337
353
|
/** User attributes para breakdowns (registration_status, pass_type, sector, pass_status…). */
|
|
338
354
|
setUserAttributes(attributes: Record<string, string | number | boolean>): void;
|
|
355
|
+
/**
|
|
356
|
+
* Registra/actualiza una métrica (valor medible) → `feedback.metrics` del body.
|
|
357
|
+
* Persistente y sobrescribe por key. No-op si tracking off.
|
|
358
|
+
*/
|
|
359
|
+
setMetric(key: string, value: string | number | boolean): void;
|
|
339
360
|
/** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
|
|
340
361
|
reportError(error: unknown, options?: ReportErrorOptions): void;
|
|
341
362
|
/** Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. No-op si tracking off. */
|
|
@@ -373,8 +394,14 @@ declare class DeepdotsPopups {
|
|
|
373
394
|
onBackground(): void;
|
|
374
395
|
/** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
|
|
375
396
|
previewAnalytics(): AnalyticsEnvelope;
|
|
376
|
-
/**
|
|
377
|
-
|
|
397
|
+
/**
|
|
398
|
+
* Envía el lote acumulado de analytics y vacía el buffer.
|
|
399
|
+
* `final: true` (cierre de página/app) cambia el transporte a `sendBeacon`, que sobrevive
|
|
400
|
+
* al unload; un lote que falle por red o 5xx se re-encola para el siguiente flush.
|
|
401
|
+
*/
|
|
402
|
+
flushAnalytics(options?: {
|
|
403
|
+
final?: boolean;
|
|
404
|
+
}): void;
|
|
378
405
|
/** Emite un evento `user_engagement` con el tiempo activo acumulado (#8). */
|
|
379
406
|
private flushEngagement;
|
|
380
407
|
private analyticsIdentity;
|
package/dist/react-native.d.ts
CHANGED
|
@@ -76,6 +76,8 @@ interface AnalyticsEnvelope {
|
|
|
76
76
|
device?: DeviceInfo;
|
|
77
77
|
/** User attributes del cliente, usados para breakdowns (registration_status, pass_type, …). */
|
|
78
78
|
attributes: Record<string, string>;
|
|
79
|
+
/** Métricas del host: valores medibles persistentes → van en `feedback.metrics` del body. */
|
|
80
|
+
metrics?: Record<string, string>;
|
|
79
81
|
};
|
|
80
82
|
events: AnalyticsEvent[];
|
|
81
83
|
}
|
|
@@ -92,7 +94,8 @@ interface AnalyticsEnvelope {
|
|
|
92
94
|
* - cada entrada usa `value: string[]` (array de un elemento)
|
|
93
95
|
* - eventos: {key: nombre_evento, value: [JSON(timestamp + params)]}
|
|
94
96
|
* - identidad (user_id) → `profile` como `external-user-id`
|
|
95
|
-
* -
|
|
97
|
+
* - métricas del host (setMetric) → `feedback.metrics` (mismo shape que metadata, sin prefijo)
|
|
98
|
+
* - `answers` vacío
|
|
96
99
|
* - `text` vacío
|
|
97
100
|
*/
|
|
98
101
|
|
|
@@ -135,6 +138,14 @@ interface DeepdotsInitParams {
|
|
|
135
138
|
storage?: KeyValueStorage;
|
|
136
139
|
/** Platform used in the analytics envelope. Default is 'web'; in RN pass 'android'/'ios'. */
|
|
137
140
|
platform?: 'web' | 'android' | 'ios';
|
|
141
|
+
/**
|
|
142
|
+
* BCP-47 language tag (e.g. 'es-ES') used for the analytics context AND for popup
|
|
143
|
+
* language segmentation (`segments.lang`). If omitted, the SDK auto-detects:
|
|
144
|
+
* `navigator.language` on web, or the `Intl` locale as a fallback (works in
|
|
145
|
+
* React Native with Hermes, where `navigator.language` is absent). If no source
|
|
146
|
+
* resolves a language, popups with `segments.lang` cannot be filtered and are shown.
|
|
147
|
+
*/
|
|
148
|
+
language?: string;
|
|
138
149
|
/** Injectable device info (RN: from react-native-device-info). If missing, it is derived from the browser. */
|
|
139
150
|
device?: DeviceInfo;
|
|
140
151
|
/**
|
|
@@ -324,6 +335,11 @@ declare class DeepdotsPopups {
|
|
|
324
335
|
private contact;
|
|
325
336
|
/** Marca si ya se inició la navegación manual (setScreen) — para RN sin History API. */
|
|
326
337
|
private navStarted;
|
|
338
|
+
/**
|
|
339
|
+
* Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
|
|
340
|
+
* verdad para el context de analytics Y para la segmentación por idioma de los popups.
|
|
341
|
+
*/
|
|
342
|
+
private language;
|
|
327
343
|
/** Initialize the SDK with configuration */
|
|
328
344
|
init(config: DeepdotsInitParams): void;
|
|
329
345
|
/** User id actual (generado por el SDK o provisto por el cliente). Null si tracking off. */
|
|
@@ -336,6 +352,11 @@ declare class DeepdotsPopups {
|
|
|
336
352
|
track(name: string, params?: Record<string, unknown>): void;
|
|
337
353
|
/** User attributes para breakdowns (registration_status, pass_type, sector, pass_status…). */
|
|
338
354
|
setUserAttributes(attributes: Record<string, string | number | boolean>): void;
|
|
355
|
+
/**
|
|
356
|
+
* Registra/actualiza una métrica (valor medible) → `feedback.metrics` del body.
|
|
357
|
+
* Persistente y sobrescribe por key. No-op si tracking off.
|
|
358
|
+
*/
|
|
359
|
+
setMetric(key: string, value: string | number | boolean): void;
|
|
339
360
|
/** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
|
|
340
361
|
reportError(error: unknown, options?: ReportErrorOptions): void;
|
|
341
362
|
/** Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. No-op si tracking off. */
|
|
@@ -373,8 +394,14 @@ declare class DeepdotsPopups {
|
|
|
373
394
|
onBackground(): void;
|
|
374
395
|
/** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
|
|
375
396
|
previewAnalytics(): AnalyticsEnvelope;
|
|
376
|
-
/**
|
|
377
|
-
|
|
397
|
+
/**
|
|
398
|
+
* Envía el lote acumulado de analytics y vacía el buffer.
|
|
399
|
+
* `final: true` (cierre de página/app) cambia el transporte a `sendBeacon`, que sobrevive
|
|
400
|
+
* al unload; un lote que falle por red o 5xx se re-encola para el siguiente flush.
|
|
401
|
+
*/
|
|
402
|
+
flushAnalytics(options?: {
|
|
403
|
+
final?: boolean;
|
|
404
|
+
}): void;
|
|
378
405
|
/** Emite un evento `user_engagement` con el tiempo activo acumulado (#8). */
|
|
379
406
|
private flushEngagement;
|
|
380
407
|
private analyticsIdentity;
|
package/dist/react-native.js
CHANGED
|
@@ -143,8 +143,8 @@ var init_tracking_manager = __esm({
|
|
|
143
143
|
|
|
144
144
|
// src/ui/font.ts
|
|
145
145
|
function fontFormatFromUrl(url) {
|
|
146
|
-
const
|
|
147
|
-
const ext =
|
|
146
|
+
const clean2 = url.split("?")[0].split("#")[0];
|
|
147
|
+
const ext = clean2.slice(clean2.lastIndexOf(".") + 1).toLowerCase();
|
|
148
148
|
return FORMAT_BY_EXT[ext];
|
|
149
149
|
}
|
|
150
150
|
function sanitizeFamily(family) {
|
|
@@ -2019,6 +2019,8 @@ var AnalyticsManager = class {
|
|
|
2019
2019
|
constructor(options = {}) {
|
|
2020
2020
|
this.events = [];
|
|
2021
2021
|
this.attributes = {};
|
|
2022
|
+
/** Métricas del host (valores medibles persistentes). Sobrescriben por key; se reenvían en cada flush. */
|
|
2023
|
+
this.metrics = {};
|
|
2022
2024
|
/** Mini-services activos: nombre → timestamp de entrada. El orden de inserción marca el "más reciente". */
|
|
2023
2025
|
this.activeMiniServices = /* @__PURE__ */ new Map();
|
|
2024
2026
|
this.sink = options.sink ?? dryRunSink;
|
|
@@ -2028,6 +2030,7 @@ var AnalyticsManager = class {
|
|
|
2028
2030
|
this.device = options.device;
|
|
2029
2031
|
this.platform = options.platform ?? "web";
|
|
2030
2032
|
this.maxBatchSize = options.maxBatchSize ?? 20;
|
|
2033
|
+
this.maxBufferedEvents = options.maxBufferedEvents ?? 200;
|
|
2031
2034
|
this.onFlushNeeded = options.onFlushNeeded;
|
|
2032
2035
|
}
|
|
2033
2036
|
/** Actualiza campos de device en runtime (p.ej. country/city tras resolver geo). */
|
|
@@ -2041,6 +2044,14 @@ var AnalyticsManager = class {
|
|
|
2041
2044
|
this.attributes[k] = String(v);
|
|
2042
2045
|
}
|
|
2043
2046
|
}
|
|
2047
|
+
/**
|
|
2048
|
+
* Registra/actualiza una métrica (valor medible). Persistente: se reenvía en cada flush.
|
|
2049
|
+
* Repetir la misma key SOBRESCRIBE; el valor se coerciona a string; ignora key vacía.
|
|
2050
|
+
*/
|
|
2051
|
+
setMetric(key, value) {
|
|
2052
|
+
if (!key) return;
|
|
2053
|
+
this.metrics[key] = String(value);
|
|
2054
|
+
}
|
|
2044
2055
|
/**
|
|
2045
2056
|
* Marca el inicio de un mini-service; etiqueta los eventos siguientes con `mini_service`.
|
|
2046
2057
|
* Admite varios activos a la vez (concurrencia); el "actual" para etiquetar es el más reciente.
|
|
@@ -2080,6 +2091,7 @@ var AnalyticsManager = class {
|
|
|
2080
2091
|
timestamp: this.now(),
|
|
2081
2092
|
params: Object.keys(merged).length ? merged : void 0
|
|
2082
2093
|
});
|
|
2094
|
+
this.enforceBufferCap();
|
|
2083
2095
|
if (this.events.length >= this.maxBatchSize) {
|
|
2084
2096
|
this.onFlushNeeded?.();
|
|
2085
2097
|
}
|
|
@@ -2104,19 +2116,49 @@ var AnalyticsManager = class {
|
|
|
2104
2116
|
platform: this.platform,
|
|
2105
2117
|
language: this.language,
|
|
2106
2118
|
device: this.device,
|
|
2107
|
-
attributes: { ...this.attributes }
|
|
2119
|
+
attributes: { ...this.attributes },
|
|
2120
|
+
metrics: { ...this.metrics }
|
|
2108
2121
|
},
|
|
2109
2122
|
events: [...this.events]
|
|
2110
2123
|
};
|
|
2111
2124
|
}
|
|
2112
|
-
/**
|
|
2113
|
-
|
|
2125
|
+
/**
|
|
2126
|
+
* Envía (vía sink) el lote acumulado y vacía el buffer. No-op si no hay eventos.
|
|
2127
|
+
*
|
|
2128
|
+
* Si el sink falla de forma transitoria (promesa rechazada o excepción síncrona) el lote
|
|
2129
|
+
* se **re-encola** al principio del buffer y se reintenta en el siguiente flush: un error
|
|
2130
|
+
* de red o un 5xx no debe hacer desaparecer los eventos.
|
|
2131
|
+
*/
|
|
2132
|
+
flush(identity, meta) {
|
|
2114
2133
|
if (this.events.length === 0) return null;
|
|
2134
|
+
const batch = this.events;
|
|
2115
2135
|
const payload = this.buildPayload(identity);
|
|
2116
|
-
this.sink(payload);
|
|
2117
2136
|
this.events = [];
|
|
2137
|
+
let result;
|
|
2138
|
+
try {
|
|
2139
|
+
result = meta ? this.sink(payload, meta) : this.sink(payload);
|
|
2140
|
+
} catch {
|
|
2141
|
+
this.requeue(batch);
|
|
2142
|
+
return payload;
|
|
2143
|
+
}
|
|
2144
|
+
if (result && typeof result.then === "function") {
|
|
2145
|
+
result.catch(() => this.requeue(batch));
|
|
2146
|
+
}
|
|
2118
2147
|
return payload;
|
|
2119
2148
|
}
|
|
2149
|
+
/**
|
|
2150
|
+
* Devuelve un lote fallido al buffer, delante de lo que haya llegado mientras se enviaba
|
|
2151
|
+
* (orden cronológico). Con el buffer lleno se descartan los eventos más antiguos.
|
|
2152
|
+
*/
|
|
2153
|
+
requeue(batch) {
|
|
2154
|
+
this.events = [...batch, ...this.events];
|
|
2155
|
+
this.enforceBufferCap();
|
|
2156
|
+
}
|
|
2157
|
+
/** Techo del buffer: si el envío lleva rato fallando, se sacrifica lo más antiguo. */
|
|
2158
|
+
enforceBufferCap() {
|
|
2159
|
+
const overflow = this.events.length - this.maxBufferedEvents;
|
|
2160
|
+
if (overflow > 0) this.events.splice(0, overflow);
|
|
2161
|
+
}
|
|
2120
2162
|
};
|
|
2121
2163
|
|
|
2122
2164
|
// src/analytics/feedback-payload.ts
|
|
@@ -2160,10 +2202,15 @@ function buildAnalyticsFeedbackBody(envelope, keys, feedbackSessionId) {
|
|
|
2160
2202
|
value: [JSON.stringify({ timestamp: e.timestamp, ...e.params ?? {} })]
|
|
2161
2203
|
});
|
|
2162
2204
|
}
|
|
2205
|
+
const metrics = [];
|
|
2206
|
+
for (const [k, v] of Object.entries(context.metrics ?? {})) {
|
|
2207
|
+
pushKV(metrics, k, v);
|
|
2208
|
+
}
|
|
2163
2209
|
return {
|
|
2164
2210
|
feedback: {
|
|
2165
2211
|
text: "",
|
|
2166
2212
|
answers: [],
|
|
2213
|
+
metrics,
|
|
2167
2214
|
metadata,
|
|
2168
2215
|
profile,
|
|
2169
2216
|
finished: false
|
|
@@ -2174,33 +2221,80 @@ function buildAnalyticsFeedbackBody(envelope, keys, feedbackSessionId) {
|
|
|
2174
2221
|
...feedbackSessionId ? { sessionId: feedbackSessionId } : {}
|
|
2175
2222
|
};
|
|
2176
2223
|
}
|
|
2224
|
+
var KEEPALIVE_MAX_BYTES = 6e4;
|
|
2225
|
+
function isRetryableStatus(status) {
|
|
2226
|
+
return status >= 500 || status === 408 || status === 429;
|
|
2227
|
+
}
|
|
2228
|
+
async function safeBodyText(res) {
|
|
2229
|
+
try {
|
|
2230
|
+
return (await res.text()).slice(0, 500);
|
|
2231
|
+
} catch {
|
|
2232
|
+
return "";
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2177
2235
|
function createFeedbackSink(options) {
|
|
2178
2236
|
let feedbackSessionId;
|
|
2179
|
-
|
|
2180
|
-
|
|
2237
|
+
let firstPostInFlight = null;
|
|
2238
|
+
const url = `${options.baseUrl}/sdk/feedback`;
|
|
2239
|
+
const post = async (body, final) => {
|
|
2240
|
+
const json = JSON.stringify(body);
|
|
2241
|
+
const small = json.length <= KEEPALIVE_MAX_BYTES;
|
|
2242
|
+
if (final && options.sendBeaconImpl && small) {
|
|
2243
|
+
const payload = typeof Blob !== "undefined" ? new Blob([json], { type: "application/json" }) : json;
|
|
2244
|
+
if (options.sendBeaconImpl(url, payload)) {
|
|
2245
|
+
options.log?.("[DeepdotsAnalytics] flush final v\xEDa sendBeacon");
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
options.log?.("[DeepdotsAnalytics] sendBeacon rechaz\xF3 el lote; fallback a fetch");
|
|
2249
|
+
}
|
|
2181
2250
|
const f = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
|
|
2182
2251
|
if (!f) {
|
|
2183
2252
|
options.log?.("[DeepdotsAnalytics] no fetch disponible; payload no enviado", body);
|
|
2184
2253
|
return;
|
|
2185
2254
|
}
|
|
2186
|
-
|
|
2187
|
-
f(`${options.baseUrl}/sdk/feedback`, {
|
|
2255
|
+
const res = await f(url, {
|
|
2188
2256
|
method: "POST",
|
|
2189
2257
|
headers: { "Content-Type": "application/json" },
|
|
2190
|
-
body:
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
options.onSessionId?.(feedbackSessionId);
|
|
2199
|
-
}
|
|
2200
|
-
} catch {
|
|
2201
|
-
}
|
|
2258
|
+
body: json,
|
|
2259
|
+
// keepalive: el POST sobrevive a la navegación mientras el body sea pequeño.
|
|
2260
|
+
...small ? { keepalive: true } : {}
|
|
2261
|
+
});
|
|
2262
|
+
if (!res.ok) {
|
|
2263
|
+
const detail = await safeBodyText(res);
|
|
2264
|
+
if (isRetryableStatus(res.status)) {
|
|
2265
|
+
throw new Error(`POST /sdk/feedback ${res.status} (reintentable): ${detail}`);
|
|
2202
2266
|
}
|
|
2203
|
-
|
|
2267
|
+
options.log?.(
|
|
2268
|
+
`[DeepdotsAnalytics] POST /sdk/feedback rechazado con ${res.status}; lote DESCARTADO:`,
|
|
2269
|
+
detail
|
|
2270
|
+
);
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
try {
|
|
2274
|
+
const data = await res.json();
|
|
2275
|
+
if (data?.sessionId && data.sessionId !== feedbackSessionId) {
|
|
2276
|
+
feedbackSessionId = data.sessionId;
|
|
2277
|
+
options.log?.("[DeepdotsAnalytics] feedbackSessionId cacheado:", feedbackSessionId);
|
|
2278
|
+
options.onSessionId?.(feedbackSessionId);
|
|
2279
|
+
}
|
|
2280
|
+
} catch {
|
|
2281
|
+
}
|
|
2282
|
+
};
|
|
2283
|
+
return async (envelope, meta) => {
|
|
2284
|
+
const final = meta?.final === true;
|
|
2285
|
+
if (!feedbackSessionId && firstPostInFlight && !final) {
|
|
2286
|
+
await firstPostInFlight;
|
|
2287
|
+
}
|
|
2288
|
+
const body = buildAnalyticsFeedbackBody(envelope, options.keys, feedbackSessionId);
|
|
2289
|
+
options.log?.("[DeepdotsAnalytics] POST /sdk/feedback \u2192", body);
|
|
2290
|
+
const sent = post(body, final);
|
|
2291
|
+
if (!feedbackSessionId) {
|
|
2292
|
+
firstPostInFlight = sent.then(
|
|
2293
|
+
() => void 0,
|
|
2294
|
+
() => void 0
|
|
2295
|
+
);
|
|
2296
|
+
}
|
|
2297
|
+
await sent;
|
|
2204
2298
|
};
|
|
2205
2299
|
}
|
|
2206
2300
|
|
|
@@ -2344,21 +2438,96 @@ function collectDeviceInfo(appVersion) {
|
|
|
2344
2438
|
}
|
|
2345
2439
|
|
|
2346
2440
|
// src/analytics/geo-info.ts
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2441
|
+
var GEO_STORAGE_KEY = "deepdots.geo";
|
|
2442
|
+
var GEO_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
2443
|
+
var GEO_TIMEOUT_MS = 3e3;
|
|
2444
|
+
function readCachedGeo(storage, now) {
|
|
2445
|
+
try {
|
|
2446
|
+
const raw = storage.getItem(GEO_STORAGE_KEY);
|
|
2447
|
+
if (!raw) return null;
|
|
2448
|
+
const cached = JSON.parse(raw);
|
|
2449
|
+
if (typeof cached.ts !== "number" || now - cached.ts > GEO_TTL_MS) return null;
|
|
2450
|
+
const geo = {};
|
|
2451
|
+
if (cached.country) geo.country = cached.country;
|
|
2452
|
+
if (cached.city) geo.city = cached.city;
|
|
2453
|
+
return geo.country || geo.city ? geo : null;
|
|
2454
|
+
} catch {
|
|
2455
|
+
return null;
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
function writeCachedGeo(storage, geo, now) {
|
|
2459
|
+
try {
|
|
2460
|
+
const payload = { ...geo, ts: now };
|
|
2461
|
+
storage.setItem(GEO_STORAGE_KEY, JSON.stringify(payload));
|
|
2462
|
+
} catch {
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
var DEFAULT_GEO_PROVIDERS = [
|
|
2466
|
+
{
|
|
2467
|
+
// https://ipapi.co/json/
|
|
2468
|
+
url: "https://ipapi.co/json/",
|
|
2469
|
+
parse: (d) => geoFrom(d.country_code, d.city)
|
|
2470
|
+
},
|
|
2471
|
+
{
|
|
2472
|
+
// https://ipwho.is/ (devuelve success:false con 200 en errores)
|
|
2473
|
+
url: "https://ipwho.is/",
|
|
2474
|
+
parse: (d) => d.success === false ? null : geoFrom(d.country_code, d.city)
|
|
2475
|
+
},
|
|
2476
|
+
{
|
|
2477
|
+
// https://ipinfo.io/json (country = código ISO)
|
|
2478
|
+
url: "https://ipinfo.io/json",
|
|
2479
|
+
parse: (d) => geoFrom(d.country, d.city)
|
|
2480
|
+
}
|
|
2481
|
+
];
|
|
2482
|
+
function geoFrom(country, city) {
|
|
2483
|
+
const c = typeof country === "string" && country ? country : void 0;
|
|
2484
|
+
const t = typeof city === "string" && city ? city : void 0;
|
|
2485
|
+
if (!c && !t) return null;
|
|
2486
|
+
return { country: c, city: t };
|
|
2487
|
+
}
|
|
2488
|
+
async function fetchOne(provider, f, timeoutMs) {
|
|
2489
|
+
const controller = new AbortController();
|
|
2490
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
2350
2491
|
try {
|
|
2351
|
-
const res = await f(
|
|
2492
|
+
const res = await f(provider.url, { signal: controller.signal });
|
|
2352
2493
|
if (!res.ok) return null;
|
|
2353
2494
|
const data = await res.json();
|
|
2354
|
-
|
|
2355
|
-
const city = data.city || void 0;
|
|
2356
|
-
if (!country && !city) return null;
|
|
2357
|
-
return { country, city };
|
|
2495
|
+
return provider.parse(data);
|
|
2358
2496
|
} catch {
|
|
2359
2497
|
return null;
|
|
2498
|
+
} finally {
|
|
2499
|
+
clearTimeout(timer);
|
|
2360
2500
|
}
|
|
2361
2501
|
}
|
|
2502
|
+
async function fetchGeo(options = {}) {
|
|
2503
|
+
const f = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
|
|
2504
|
+
if (!f) return null;
|
|
2505
|
+
const providers = options.providers ?? DEFAULT_GEO_PROVIDERS;
|
|
2506
|
+
const timeoutMs = options.timeoutMs ?? GEO_TIMEOUT_MS;
|
|
2507
|
+
for (const provider of providers) {
|
|
2508
|
+
const geo = await fetchOne(provider, f, timeoutMs);
|
|
2509
|
+
if (geo) return geo;
|
|
2510
|
+
}
|
|
2511
|
+
return null;
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
// src/analytics/language.ts
|
|
2515
|
+
function clean(value) {
|
|
2516
|
+
const trimmed = value?.trim();
|
|
2517
|
+
return trimmed ? trimmed : void 0;
|
|
2518
|
+
}
|
|
2519
|
+
function resolveLanguage(sources) {
|
|
2520
|
+
const explicit = clean(sources.explicit);
|
|
2521
|
+
if (explicit) return explicit;
|
|
2522
|
+
const navLang = clean(sources.navigator?.language);
|
|
2523
|
+
if (navLang) return navLang;
|
|
2524
|
+
try {
|
|
2525
|
+
const locale = clean(sources.intl?.DateTimeFormat().resolvedOptions().locale);
|
|
2526
|
+
if (locale) return locale;
|
|
2527
|
+
} catch {
|
|
2528
|
+
}
|
|
2529
|
+
return void 0;
|
|
2530
|
+
}
|
|
2362
2531
|
|
|
2363
2532
|
// src/analytics/engagement-tracker.ts
|
|
2364
2533
|
var EngagementTracker = class {
|
|
@@ -2619,6 +2788,11 @@ var DeepdotsPopups = class {
|
|
|
2619
2788
|
this.contact = null;
|
|
2620
2789
|
/** Marca si ya se inició la navegación manual (setScreen) — para RN sin History API. */
|
|
2621
2790
|
this.navStarted = false;
|
|
2791
|
+
/**
|
|
2792
|
+
* Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
|
|
2793
|
+
* verdad para el context de analytics Y para la segmentación por idioma de los popups.
|
|
2794
|
+
*/
|
|
2795
|
+
this.language = void 0;
|
|
2622
2796
|
}
|
|
2623
2797
|
/** Initialize the SDK with configuration */
|
|
2624
2798
|
init(config) {
|
|
@@ -2660,15 +2834,22 @@ var DeepdotsPopups = class {
|
|
|
2660
2834
|
baseUrl: this.baseUrl,
|
|
2661
2835
|
keys: config.analytics,
|
|
2662
2836
|
log: (...a) => this.log(...a),
|
|
2837
|
+
// sendBeacon: único transporte que sobrevive al cierre de la página.
|
|
2838
|
+
sendBeaconImpl: typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" ? (url, body) => navigator.sendBeacon(url, body) : void 0,
|
|
2663
2839
|
onSessionId: (id) => {
|
|
2664
2840
|
this.analyticsFeedbackSessionId = id;
|
|
2665
2841
|
}
|
|
2666
2842
|
}) : createDryRunSink((...a) => this.logger.log(...a));
|
|
2667
2843
|
const device = config.device ?? collectDeviceInfo(config.appVersion);
|
|
2844
|
+
this.language = resolveLanguage({
|
|
2845
|
+
explicit: config.language,
|
|
2846
|
+
navigator: typeof navigator !== "undefined" ? navigator : void 0,
|
|
2847
|
+
intl: typeof Intl !== "undefined" ? Intl : void 0
|
|
2848
|
+
});
|
|
2668
2849
|
this.analytics = new AnalyticsManager({
|
|
2669
2850
|
sink: analyticsSink,
|
|
2670
2851
|
publicKey: config.analytics?.publicKey ?? this.config.apiKey,
|
|
2671
|
-
language:
|
|
2852
|
+
language: this.language,
|
|
2672
2853
|
platform: config.platform ?? "web",
|
|
2673
2854
|
device,
|
|
2674
2855
|
maxBatchSize: ANALYTICS_MAX_BATCH_SIZE,
|
|
@@ -2685,8 +2866,13 @@ var DeepdotsPopups = class {
|
|
|
2685
2866
|
if (this.tracking?.isTrackingEnabled()) {
|
|
2686
2867
|
this.crashReporter.install();
|
|
2687
2868
|
}
|
|
2688
|
-
|
|
2689
|
-
|
|
2869
|
+
const cachedGeo = readCachedGeo(storage, Date.now());
|
|
2870
|
+
if (cachedGeo) this.analytics.updateDevice(cachedGeo);
|
|
2871
|
+
fetchGeo().then((geo) => {
|
|
2872
|
+
if (geo) {
|
|
2873
|
+
this.analytics?.updateDevice(geo);
|
|
2874
|
+
writeCachedGeo(storage, geo, Date.now());
|
|
2875
|
+
}
|
|
2690
2876
|
}).catch(() => {
|
|
2691
2877
|
});
|
|
2692
2878
|
this.navObserver = new NavigationObserver();
|
|
@@ -2743,6 +2929,15 @@ var DeepdotsPopups = class {
|
|
|
2743
2929
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
2744
2930
|
this.analytics?.setUserAttributes(attributes);
|
|
2745
2931
|
}
|
|
2932
|
+
/**
|
|
2933
|
+
* Registra/actualiza una métrica (valor medible) → `feedback.metrics` del body.
|
|
2934
|
+
* Persistente y sobrescribe por key. No-op si tracking off.
|
|
2935
|
+
*/
|
|
2936
|
+
setMetric(key, value) {
|
|
2937
|
+
if (!this.tracking?.isTrackingEnabled()) return;
|
|
2938
|
+
this.analytics?.setMetric(key, value);
|
|
2939
|
+
this.log("analytics \xB7 setMetric:", key, value);
|
|
2940
|
+
}
|
|
2746
2941
|
/** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
|
|
2747
2942
|
reportError(error, options) {
|
|
2748
2943
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
@@ -2830,10 +3025,14 @@ var DeepdotsPopups = class {
|
|
|
2830
3025
|
events: []
|
|
2831
3026
|
};
|
|
2832
3027
|
}
|
|
2833
|
-
/**
|
|
2834
|
-
|
|
3028
|
+
/**
|
|
3029
|
+
* Envía el lote acumulado de analytics y vacía el buffer.
|
|
3030
|
+
* `final: true` (cierre de página/app) cambia el transporte a `sendBeacon`, que sobrevive
|
|
3031
|
+
* al unload; un lote que falle por red o 5xx se re-encola para el siguiente flush.
|
|
3032
|
+
*/
|
|
3033
|
+
flushAnalytics(options) {
|
|
2835
3034
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
2836
|
-
this.analytics?.flush(this.analyticsIdentity());
|
|
3035
|
+
this.analytics?.flush(this.analyticsIdentity(), options);
|
|
2837
3036
|
}
|
|
2838
3037
|
/** Emite un evento `user_engagement` con el tiempo activo acumulado (#8). */
|
|
2839
3038
|
flushEngagement() {
|
|
@@ -2856,7 +3055,7 @@ var DeepdotsPopups = class {
|
|
|
2856
3055
|
this.navObserver?.stop();
|
|
2857
3056
|
if (this.tracking?.isTrackingEnabled()) this.analytics?.exitAllMiniServices();
|
|
2858
3057
|
this.flushEngagement();
|
|
2859
|
-
this.flushAnalytics();
|
|
3058
|
+
this.flushAnalytics({ final: true });
|
|
2860
3059
|
});
|
|
2861
3060
|
document.addEventListener("visibilitychange", () => {
|
|
2862
3061
|
if (document.visibilityState === "hidden") {
|
|
@@ -2977,14 +3176,16 @@ var DeepdotsPopups = class {
|
|
|
2977
3176
|
matchesSegmentsLang(def) {
|
|
2978
3177
|
const langs = def.segments?.lang;
|
|
2979
3178
|
if (!langs || langs.length === 0) return true;
|
|
2980
|
-
const
|
|
2981
|
-
if (!
|
|
2982
|
-
this.debug("No
|
|
3179
|
+
const currentLang = (this.language ?? "").toLowerCase();
|
|
3180
|
+
if (!currentLang) {
|
|
3181
|
+
this.debug("No language available for lang comparison", { popupId: def.id, langs });
|
|
2983
3182
|
return true;
|
|
2984
3183
|
}
|
|
2985
|
-
const matches = langs.some(
|
|
3184
|
+
const matches = langs.some(
|
|
3185
|
+
(lang) => typeof lang === "string" && currentLang.startsWith(lang.toLowerCase())
|
|
3186
|
+
);
|
|
2986
3187
|
if (!matches) {
|
|
2987
|
-
this.debug("Lang mismatch for popup", { popupId: def.id, langs,
|
|
3188
|
+
this.debug("Lang mismatch for popup", { popupId: def.id, langs, currentLang });
|
|
2988
3189
|
}
|
|
2989
3190
|
return matches;
|
|
2990
3191
|
}
|