@magicfeedback/popup-sdk 1.1.7 → 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 +50 -10
- package/dist/index.d.ts +50 -10
- package/dist/index.js +126 -34
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +126 -34
- package/dist/index.mjs.map +1 -1
- package/dist/react-native.d.mts +18 -5
- package/dist/react-native.d.ts +18 -5
- package/dist/react-native.js +126 -34
- package/dist/react-native.js.map +1 -1
- package/dist/react-native.mjs +126 -34
- package/dist/react-native.mjs.map +1 -1
- package/package.json +1 -1
package/dist/react-native.d.mts
CHANGED
|
@@ -139,9 +139,11 @@ interface DeepdotsInitParams {
|
|
|
139
139
|
/** Platform used in the analytics envelope. Default is 'web'; in RN pass 'android'/'ios'. */
|
|
140
140
|
platform?: 'web' | 'android' | 'ios';
|
|
141
141
|
/**
|
|
142
|
-
* BCP-47 language tag
|
|
143
|
-
*
|
|
144
|
-
*
|
|
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.
|
|
145
147
|
*/
|
|
146
148
|
language?: string;
|
|
147
149
|
/** Injectable device info (RN: from react-native-device-info). If missing, it is derived from the browser. */
|
|
@@ -333,6 +335,11 @@ declare class DeepdotsPopups {
|
|
|
333
335
|
private contact;
|
|
334
336
|
/** Marca si ya se inició la navegación manual (setScreen) — para RN sin History API. */
|
|
335
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;
|
|
336
343
|
/** Initialize the SDK with configuration */
|
|
337
344
|
init(config: DeepdotsInitParams): void;
|
|
338
345
|
/** User id actual (generado por el SDK o provisto por el cliente). Null si tracking off. */
|
|
@@ -387,8 +394,14 @@ declare class DeepdotsPopups {
|
|
|
387
394
|
onBackground(): void;
|
|
388
395
|
/** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
|
|
389
396
|
previewAnalytics(): AnalyticsEnvelope;
|
|
390
|
-
/**
|
|
391
|
-
|
|
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;
|
|
392
405
|
/** Emite un evento `user_engagement` con el tiempo activo acumulado (#8). */
|
|
393
406
|
private flushEngagement;
|
|
394
407
|
private analyticsIdentity;
|
package/dist/react-native.d.ts
CHANGED
|
@@ -139,9 +139,11 @@ interface DeepdotsInitParams {
|
|
|
139
139
|
/** Platform used in the analytics envelope. Default is 'web'; in RN pass 'android'/'ios'. */
|
|
140
140
|
platform?: 'web' | 'android' | 'ios';
|
|
141
141
|
/**
|
|
142
|
-
* BCP-47 language tag
|
|
143
|
-
*
|
|
144
|
-
*
|
|
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.
|
|
145
147
|
*/
|
|
146
148
|
language?: string;
|
|
147
149
|
/** Injectable device info (RN: from react-native-device-info). If missing, it is derived from the browser. */
|
|
@@ -333,6 +335,11 @@ declare class DeepdotsPopups {
|
|
|
333
335
|
private contact;
|
|
334
336
|
/** Marca si ya se inició la navegación manual (setScreen) — para RN sin History API. */
|
|
335
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;
|
|
336
343
|
/** Initialize the SDK with configuration */
|
|
337
344
|
init(config: DeepdotsInitParams): void;
|
|
338
345
|
/** User id actual (generado por el SDK o provisto por el cliente). Null si tracking off. */
|
|
@@ -387,8 +394,14 @@ declare class DeepdotsPopups {
|
|
|
387
394
|
onBackground(): void;
|
|
388
395
|
/** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
|
|
389
396
|
previewAnalytics(): AnalyticsEnvelope;
|
|
390
|
-
/**
|
|
391
|
-
|
|
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;
|
|
392
405
|
/** Emite un evento `user_engagement` con el tiempo activo acumulado (#8). */
|
|
393
406
|
private flushEngagement;
|
|
394
407
|
private analyticsIdentity;
|
package/dist/react-native.js
CHANGED
|
@@ -2030,6 +2030,7 @@ var AnalyticsManager = class {
|
|
|
2030
2030
|
this.device = options.device;
|
|
2031
2031
|
this.platform = options.platform ?? "web";
|
|
2032
2032
|
this.maxBatchSize = options.maxBatchSize ?? 20;
|
|
2033
|
+
this.maxBufferedEvents = options.maxBufferedEvents ?? 200;
|
|
2033
2034
|
this.onFlushNeeded = options.onFlushNeeded;
|
|
2034
2035
|
}
|
|
2035
2036
|
/** Actualiza campos de device en runtime (p.ej. country/city tras resolver geo). */
|
|
@@ -2090,6 +2091,7 @@ var AnalyticsManager = class {
|
|
|
2090
2091
|
timestamp: this.now(),
|
|
2091
2092
|
params: Object.keys(merged).length ? merged : void 0
|
|
2092
2093
|
});
|
|
2094
|
+
this.enforceBufferCap();
|
|
2093
2095
|
if (this.events.length >= this.maxBatchSize) {
|
|
2094
2096
|
this.onFlushNeeded?.();
|
|
2095
2097
|
}
|
|
@@ -2120,14 +2122,43 @@ var AnalyticsManager = class {
|
|
|
2120
2122
|
events: [...this.events]
|
|
2121
2123
|
};
|
|
2122
2124
|
}
|
|
2123
|
-
/**
|
|
2124
|
-
|
|
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) {
|
|
2125
2133
|
if (this.events.length === 0) return null;
|
|
2134
|
+
const batch = this.events;
|
|
2126
2135
|
const payload = this.buildPayload(identity);
|
|
2127
|
-
this.sink(payload);
|
|
2128
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
|
+
}
|
|
2129
2147
|
return payload;
|
|
2130
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
|
+
}
|
|
2131
2162
|
};
|
|
2132
2163
|
|
|
2133
2164
|
// src/analytics/feedback-payload.ts
|
|
@@ -2190,33 +2221,80 @@ function buildAnalyticsFeedbackBody(envelope, keys, feedbackSessionId) {
|
|
|
2190
2221
|
...feedbackSessionId ? { sessionId: feedbackSessionId } : {}
|
|
2191
2222
|
};
|
|
2192
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
|
+
}
|
|
2193
2235
|
function createFeedbackSink(options) {
|
|
2194
2236
|
let feedbackSessionId;
|
|
2195
|
-
|
|
2196
|
-
|
|
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
|
+
}
|
|
2197
2250
|
const f = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
|
|
2198
2251
|
if (!f) {
|
|
2199
2252
|
options.log?.("[DeepdotsAnalytics] no fetch disponible; payload no enviado", body);
|
|
2200
2253
|
return;
|
|
2201
2254
|
}
|
|
2202
|
-
|
|
2203
|
-
f(`${options.baseUrl}/sdk/feedback`, {
|
|
2255
|
+
const res = await f(url, {
|
|
2204
2256
|
method: "POST",
|
|
2205
2257
|
headers: { "Content-Type": "application/json" },
|
|
2206
|
-
body:
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
}
|
|
2217
|
-
|
|
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}`);
|
|
2266
|
+
}
|
|
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);
|
|
2218
2279
|
}
|
|
2219
|
-
}
|
|
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;
|
|
2220
2298
|
};
|
|
2221
2299
|
}
|
|
2222
2300
|
|
|
@@ -2710,6 +2788,11 @@ var DeepdotsPopups = class {
|
|
|
2710
2788
|
this.contact = null;
|
|
2711
2789
|
/** Marca si ya se inició la navegación manual (setScreen) — para RN sin History API. */
|
|
2712
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;
|
|
2713
2796
|
}
|
|
2714
2797
|
/** Initialize the SDK with configuration */
|
|
2715
2798
|
init(config) {
|
|
@@ -2751,19 +2834,22 @@ var DeepdotsPopups = class {
|
|
|
2751
2834
|
baseUrl: this.baseUrl,
|
|
2752
2835
|
keys: config.analytics,
|
|
2753
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,
|
|
2754
2839
|
onSessionId: (id) => {
|
|
2755
2840
|
this.analyticsFeedbackSessionId = id;
|
|
2756
2841
|
}
|
|
2757
2842
|
}) : createDryRunSink((...a) => this.logger.log(...a));
|
|
2758
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
|
+
});
|
|
2759
2849
|
this.analytics = new AnalyticsManager({
|
|
2760
2850
|
sink: analyticsSink,
|
|
2761
2851
|
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
|
-
}),
|
|
2852
|
+
language: this.language,
|
|
2767
2853
|
platform: config.platform ?? "web",
|
|
2768
2854
|
device,
|
|
2769
2855
|
maxBatchSize: ANALYTICS_MAX_BATCH_SIZE,
|
|
@@ -2939,10 +3025,14 @@ var DeepdotsPopups = class {
|
|
|
2939
3025
|
events: []
|
|
2940
3026
|
};
|
|
2941
3027
|
}
|
|
2942
|
-
/**
|
|
2943
|
-
|
|
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) {
|
|
2944
3034
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
2945
|
-
this.analytics?.flush(this.analyticsIdentity());
|
|
3035
|
+
this.analytics?.flush(this.analyticsIdentity(), options);
|
|
2946
3036
|
}
|
|
2947
3037
|
/** Emite un evento `user_engagement` con el tiempo activo acumulado (#8). */
|
|
2948
3038
|
flushEngagement() {
|
|
@@ -2965,7 +3055,7 @@ var DeepdotsPopups = class {
|
|
|
2965
3055
|
this.navObserver?.stop();
|
|
2966
3056
|
if (this.tracking?.isTrackingEnabled()) this.analytics?.exitAllMiniServices();
|
|
2967
3057
|
this.flushEngagement();
|
|
2968
|
-
this.flushAnalytics();
|
|
3058
|
+
this.flushAnalytics({ final: true });
|
|
2969
3059
|
});
|
|
2970
3060
|
document.addEventListener("visibilitychange", () => {
|
|
2971
3061
|
if (document.visibilityState === "hidden") {
|
|
@@ -3086,14 +3176,16 @@ var DeepdotsPopups = class {
|
|
|
3086
3176
|
matchesSegmentsLang(def) {
|
|
3087
3177
|
const langs = def.segments?.lang;
|
|
3088
3178
|
if (!langs || langs.length === 0) return true;
|
|
3089
|
-
const
|
|
3090
|
-
if (!
|
|
3091
|
-
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 });
|
|
3092
3182
|
return true;
|
|
3093
3183
|
}
|
|
3094
|
-
const matches = langs.some(
|
|
3184
|
+
const matches = langs.some(
|
|
3185
|
+
(lang) => typeof lang === "string" && currentLang.startsWith(lang.toLowerCase())
|
|
3186
|
+
);
|
|
3095
3187
|
if (!matches) {
|
|
3096
|
-
this.debug("Lang mismatch for popup", { popupId: def.id, langs,
|
|
3188
|
+
this.debug("Lang mismatch for popup", { popupId: def.id, langs, currentLang });
|
|
3097
3189
|
}
|
|
3098
3190
|
return matches;
|
|
3099
3191
|
}
|