@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/README.md
CHANGED
|
@@ -177,6 +177,11 @@ Config fields:
|
|
|
177
177
|
- `mode?: 'server' | 'client'`
|
|
178
178
|
- `debug?: boolean`
|
|
179
179
|
- `popups?: PopupDefinition[]`
|
|
180
|
+
- `language?: string` — BCP-47 tag (`'en'`, `'en-US'`, …) used for popup language
|
|
181
|
+
segmentation (`segments.lang`) and for the analytics context. If omitted it is
|
|
182
|
+
auto-detected from `navigator.language`, falling back to the `Intl` locale (which is
|
|
183
|
+
what makes it work in React Native, where `navigator.language` does not exist). If no
|
|
184
|
+
source resolves a language, popups with `segments.lang` cannot be filtered and are shown.
|
|
180
185
|
|
|
181
186
|
### `autoLaunch()`
|
|
182
187
|
|
package/dist/index.d.mts
CHANGED
|
@@ -91,7 +91,20 @@ interface AnalyticsEnvelope {
|
|
|
91
91
|
};
|
|
92
92
|
events: AnalyticsEvent[];
|
|
93
93
|
}
|
|
94
|
-
|
|
94
|
+
/** Contexto del flush, para que el sink pueda adaptar el transporte. */
|
|
95
|
+
interface AnalyticsFlushMeta {
|
|
96
|
+
/**
|
|
97
|
+
* `true` cuando el flush ocurre porque la página/app se está cerrando. El sink debe usar
|
|
98
|
+
* un transporte que sobreviva al unload (sendBeacon) y no puede esperar respuestas.
|
|
99
|
+
*/
|
|
100
|
+
final?: boolean;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Envío del lote. Si devuelve una promesa que **rechaza**, el manager re-encola los
|
|
104
|
+
* eventos para reintentarlos en el siguiente flush (fallo transitorio: red o 5xx).
|
|
105
|
+
* Resolver = lote entregado o descartado definitivamente (no se reintenta).
|
|
106
|
+
*/
|
|
107
|
+
type AnalyticsSink = (payload: AnalyticsEnvelope, meta?: AnalyticsFlushMeta) => void | Promise<void>;
|
|
95
108
|
|
|
96
109
|
/**
|
|
97
110
|
* Mapeo del envelope de analytics → body de `POST /sdk/feedback`.
|
|
@@ -143,15 +156,29 @@ interface FeedbackSinkOptions {
|
|
|
143
156
|
log?: (...args: unknown[]) => void;
|
|
144
157
|
/** fetch inyectable (tests / RN). */
|
|
145
158
|
fetchImpl?: typeof fetch;
|
|
159
|
+
/**
|
|
160
|
+
* `navigator.sendBeacon` inyectable. Solo se usa en el flush final (cierre de página):
|
|
161
|
+
* sobrevive al unload, a cambio de no poder leer la respuesta. Si no se pasa, el flush
|
|
162
|
+
* final usa `fetch` con `keepalive`.
|
|
163
|
+
*/
|
|
164
|
+
sendBeaconImpl?: (url: string, body: Blob | string) => boolean;
|
|
146
165
|
/** Callback invocado cuando el backend devuelve un nuevo sessionId (primer POST). */
|
|
147
166
|
onSessionId?: (id: string) => void;
|
|
148
167
|
}
|
|
149
168
|
/**
|
|
150
169
|
* Sink real: transforma el envelope y hace `POST {baseUrl}/sdk/feedback`.
|
|
151
|
-
*
|
|
152
|
-
* Stateful: cachea el `sessionId` que devuelve el backend en la primera respuesta
|
|
153
|
-
*
|
|
154
|
-
*
|
|
170
|
+
*
|
|
171
|
+
* Stateful: cachea el `sessionId` que devuelve el backend en la primera respuesta y lo
|
|
172
|
+
* envía en los siguientes POSTs para que el backend agrupe todos los eventos en un único
|
|
173
|
+
* registro de feedback.
|
|
174
|
+
*
|
|
175
|
+
* Garantías de entrega:
|
|
176
|
+
* - mientras no se conozca el `sessionId`, los lotes se **serializan** (esperan la primera
|
|
177
|
+
* respuesta): dos POSTs a la vez sin `sessionId` crearían dos registros y partirían los datos;
|
|
178
|
+
* - `keepalive` para que el navegador no cancele el POST al navegar fuera, y `sendBeacon`
|
|
179
|
+
* en el flush final (cierre de página), donde `fetch` puede morir con el documento;
|
|
180
|
+
* - la promesa **rechaza** en fallo transitorio (red, 5xx, 408, 429) para que el
|
|
181
|
+
* `AnalyticsManager` re-encole el lote; un 4xx se loguea y se descarta.
|
|
155
182
|
*/
|
|
156
183
|
declare function createFeedbackSink(options: FeedbackSinkOptions): AnalyticsSink;
|
|
157
184
|
|
|
@@ -190,9 +217,11 @@ interface DeepdotsInitParams {
|
|
|
190
217
|
/** Platform used in the analytics envelope. Default is 'web'; in RN pass 'android'/'ios'. */
|
|
191
218
|
platform?: 'web' | 'android' | 'ios';
|
|
192
219
|
/**
|
|
193
|
-
* BCP-47 language tag
|
|
194
|
-
*
|
|
195
|
-
*
|
|
220
|
+
* BCP-47 language tag (e.g. 'es-ES') used for the analytics context AND for popup
|
|
221
|
+
* language segmentation (`segments.lang`). If omitted, the SDK auto-detects:
|
|
222
|
+
* `navigator.language` on web, or the `Intl` locale as a fallback (works in
|
|
223
|
+
* React Native with Hermes, where `navigator.language` is absent). If no source
|
|
224
|
+
* resolves a language, popups with `segments.lang` cannot be filtered and are shown.
|
|
196
225
|
*/
|
|
197
226
|
language?: string;
|
|
198
227
|
/** Injectable device info (RN: from react-native-device-info). If missing, it is derived from the browser. */
|
|
@@ -527,6 +556,11 @@ declare class DeepdotsPopups {
|
|
|
527
556
|
private contact;
|
|
528
557
|
/** Marca si ya se inició la navegación manual (setScreen) — para RN sin History API. */
|
|
529
558
|
private navStarted;
|
|
559
|
+
/**
|
|
560
|
+
* Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
|
|
561
|
+
* verdad para el context de analytics Y para la segmentación por idioma de los popups.
|
|
562
|
+
*/
|
|
563
|
+
private language;
|
|
530
564
|
/** Initialize the SDK with configuration */
|
|
531
565
|
init(config: DeepdotsInitParams): void;
|
|
532
566
|
/** User id actual (generado por el SDK o provisto por el cliente). Null si tracking off. */
|
|
@@ -581,8 +615,14 @@ declare class DeepdotsPopups {
|
|
|
581
615
|
onBackground(): void;
|
|
582
616
|
/** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
|
|
583
617
|
previewAnalytics(): AnalyticsEnvelope;
|
|
584
|
-
/**
|
|
585
|
-
|
|
618
|
+
/**
|
|
619
|
+
* Envía el lote acumulado de analytics y vacía el buffer.
|
|
620
|
+
* `final: true` (cierre de página/app) cambia el transporte a `sendBeacon`, que sobrevive
|
|
621
|
+
* al unload; un lote que falle por red o 5xx se re-encola para el siguiente flush.
|
|
622
|
+
*/
|
|
623
|
+
flushAnalytics(options?: {
|
|
624
|
+
final?: boolean;
|
|
625
|
+
}): void;
|
|
586
626
|
/** Emite un evento `user_engagement` con el tiempo activo acumulado (#8). */
|
|
587
627
|
private flushEngagement;
|
|
588
628
|
private analyticsIdentity;
|
package/dist/index.d.ts
CHANGED
|
@@ -91,7 +91,20 @@ interface AnalyticsEnvelope {
|
|
|
91
91
|
};
|
|
92
92
|
events: AnalyticsEvent[];
|
|
93
93
|
}
|
|
94
|
-
|
|
94
|
+
/** Contexto del flush, para que el sink pueda adaptar el transporte. */
|
|
95
|
+
interface AnalyticsFlushMeta {
|
|
96
|
+
/**
|
|
97
|
+
* `true` cuando el flush ocurre porque la página/app se está cerrando. El sink debe usar
|
|
98
|
+
* un transporte que sobreviva al unload (sendBeacon) y no puede esperar respuestas.
|
|
99
|
+
*/
|
|
100
|
+
final?: boolean;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Envío del lote. Si devuelve una promesa que **rechaza**, el manager re-encola los
|
|
104
|
+
* eventos para reintentarlos en el siguiente flush (fallo transitorio: red o 5xx).
|
|
105
|
+
* Resolver = lote entregado o descartado definitivamente (no se reintenta).
|
|
106
|
+
*/
|
|
107
|
+
type AnalyticsSink = (payload: AnalyticsEnvelope, meta?: AnalyticsFlushMeta) => void | Promise<void>;
|
|
95
108
|
|
|
96
109
|
/**
|
|
97
110
|
* Mapeo del envelope de analytics → body de `POST /sdk/feedback`.
|
|
@@ -143,15 +156,29 @@ interface FeedbackSinkOptions {
|
|
|
143
156
|
log?: (...args: unknown[]) => void;
|
|
144
157
|
/** fetch inyectable (tests / RN). */
|
|
145
158
|
fetchImpl?: typeof fetch;
|
|
159
|
+
/**
|
|
160
|
+
* `navigator.sendBeacon` inyectable. Solo se usa en el flush final (cierre de página):
|
|
161
|
+
* sobrevive al unload, a cambio de no poder leer la respuesta. Si no se pasa, el flush
|
|
162
|
+
* final usa `fetch` con `keepalive`.
|
|
163
|
+
*/
|
|
164
|
+
sendBeaconImpl?: (url: string, body: Blob | string) => boolean;
|
|
146
165
|
/** Callback invocado cuando el backend devuelve un nuevo sessionId (primer POST). */
|
|
147
166
|
onSessionId?: (id: string) => void;
|
|
148
167
|
}
|
|
149
168
|
/**
|
|
150
169
|
* Sink real: transforma el envelope y hace `POST {baseUrl}/sdk/feedback`.
|
|
151
|
-
*
|
|
152
|
-
* Stateful: cachea el `sessionId` que devuelve el backend en la primera respuesta
|
|
153
|
-
*
|
|
154
|
-
*
|
|
170
|
+
*
|
|
171
|
+
* Stateful: cachea el `sessionId` que devuelve el backend en la primera respuesta y lo
|
|
172
|
+
* envía en los siguientes POSTs para que el backend agrupe todos los eventos en un único
|
|
173
|
+
* registro de feedback.
|
|
174
|
+
*
|
|
175
|
+
* Garantías de entrega:
|
|
176
|
+
* - mientras no se conozca el `sessionId`, los lotes se **serializan** (esperan la primera
|
|
177
|
+
* respuesta): dos POSTs a la vez sin `sessionId` crearían dos registros y partirían los datos;
|
|
178
|
+
* - `keepalive` para que el navegador no cancele el POST al navegar fuera, y `sendBeacon`
|
|
179
|
+
* en el flush final (cierre de página), donde `fetch` puede morir con el documento;
|
|
180
|
+
* - la promesa **rechaza** en fallo transitorio (red, 5xx, 408, 429) para que el
|
|
181
|
+
* `AnalyticsManager` re-encole el lote; un 4xx se loguea y se descarta.
|
|
155
182
|
*/
|
|
156
183
|
declare function createFeedbackSink(options: FeedbackSinkOptions): AnalyticsSink;
|
|
157
184
|
|
|
@@ -190,9 +217,11 @@ interface DeepdotsInitParams {
|
|
|
190
217
|
/** Platform used in the analytics envelope. Default is 'web'; in RN pass 'android'/'ios'. */
|
|
191
218
|
platform?: 'web' | 'android' | 'ios';
|
|
192
219
|
/**
|
|
193
|
-
* BCP-47 language tag
|
|
194
|
-
*
|
|
195
|
-
*
|
|
220
|
+
* BCP-47 language tag (e.g. 'es-ES') used for the analytics context AND for popup
|
|
221
|
+
* language segmentation (`segments.lang`). If omitted, the SDK auto-detects:
|
|
222
|
+
* `navigator.language` on web, or the `Intl` locale as a fallback (works in
|
|
223
|
+
* React Native with Hermes, where `navigator.language` is absent). If no source
|
|
224
|
+
* resolves a language, popups with `segments.lang` cannot be filtered and are shown.
|
|
196
225
|
*/
|
|
197
226
|
language?: string;
|
|
198
227
|
/** Injectable device info (RN: from react-native-device-info). If missing, it is derived from the browser. */
|
|
@@ -527,6 +556,11 @@ declare class DeepdotsPopups {
|
|
|
527
556
|
private contact;
|
|
528
557
|
/** Marca si ya se inició la navegación manual (setScreen) — para RN sin History API. */
|
|
529
558
|
private navStarted;
|
|
559
|
+
/**
|
|
560
|
+
* Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
|
|
561
|
+
* verdad para el context de analytics Y para la segmentación por idioma de los popups.
|
|
562
|
+
*/
|
|
563
|
+
private language;
|
|
530
564
|
/** Initialize the SDK with configuration */
|
|
531
565
|
init(config: DeepdotsInitParams): void;
|
|
532
566
|
/** User id actual (generado por el SDK o provisto por el cliente). Null si tracking off. */
|
|
@@ -581,8 +615,14 @@ declare class DeepdotsPopups {
|
|
|
581
615
|
onBackground(): void;
|
|
582
616
|
/** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
|
|
583
617
|
previewAnalytics(): AnalyticsEnvelope;
|
|
584
|
-
/**
|
|
585
|
-
|
|
618
|
+
/**
|
|
619
|
+
* Envía el lote acumulado de analytics y vacía el buffer.
|
|
620
|
+
* `final: true` (cierre de página/app) cambia el transporte a `sendBeacon`, que sobrevive
|
|
621
|
+
* al unload; un lote que falle por red o 5xx se re-encola para el siguiente flush.
|
|
622
|
+
*/
|
|
623
|
+
flushAnalytics(options?: {
|
|
624
|
+
final?: boolean;
|
|
625
|
+
}): void;
|
|
586
626
|
/** Emite un evento `user_engagement` con el tiempo activo acumulado (#8). */
|
|
587
627
|
private flushEngagement;
|
|
588
628
|
private analyticsIdentity;
|
package/dist/index.js
CHANGED
|
@@ -3968,6 +3968,7 @@ var AnalyticsManager = class {
|
|
|
3968
3968
|
this.device = options.device;
|
|
3969
3969
|
this.platform = options.platform ?? "web";
|
|
3970
3970
|
this.maxBatchSize = options.maxBatchSize ?? 20;
|
|
3971
|
+
this.maxBufferedEvents = options.maxBufferedEvents ?? 200;
|
|
3971
3972
|
this.onFlushNeeded = options.onFlushNeeded;
|
|
3972
3973
|
}
|
|
3973
3974
|
/** Actualiza campos de device en runtime (p.ej. country/city tras resolver geo). */
|
|
@@ -4028,6 +4029,7 @@ var AnalyticsManager = class {
|
|
|
4028
4029
|
timestamp: this.now(),
|
|
4029
4030
|
params: Object.keys(merged).length ? merged : void 0
|
|
4030
4031
|
});
|
|
4032
|
+
this.enforceBufferCap();
|
|
4031
4033
|
if (this.events.length >= this.maxBatchSize) {
|
|
4032
4034
|
this.onFlushNeeded?.();
|
|
4033
4035
|
}
|
|
@@ -4058,14 +4060,43 @@ var AnalyticsManager = class {
|
|
|
4058
4060
|
events: [...this.events]
|
|
4059
4061
|
};
|
|
4060
4062
|
}
|
|
4061
|
-
/**
|
|
4062
|
-
|
|
4063
|
+
/**
|
|
4064
|
+
* Envía (vía sink) el lote acumulado y vacía el buffer. No-op si no hay eventos.
|
|
4065
|
+
*
|
|
4066
|
+
* Si el sink falla de forma transitoria (promesa rechazada o excepción síncrona) el lote
|
|
4067
|
+
* se **re-encola** al principio del buffer y se reintenta en el siguiente flush: un error
|
|
4068
|
+
* de red o un 5xx no debe hacer desaparecer los eventos.
|
|
4069
|
+
*/
|
|
4070
|
+
flush(identity, meta) {
|
|
4063
4071
|
if (this.events.length === 0) return null;
|
|
4072
|
+
const batch = this.events;
|
|
4064
4073
|
const payload = this.buildPayload(identity);
|
|
4065
|
-
this.sink(payload);
|
|
4066
4074
|
this.events = [];
|
|
4075
|
+
let result;
|
|
4076
|
+
try {
|
|
4077
|
+
result = meta ? this.sink(payload, meta) : this.sink(payload);
|
|
4078
|
+
} catch {
|
|
4079
|
+
this.requeue(batch);
|
|
4080
|
+
return payload;
|
|
4081
|
+
}
|
|
4082
|
+
if (result && typeof result.then === "function") {
|
|
4083
|
+
result.catch(() => this.requeue(batch));
|
|
4084
|
+
}
|
|
4067
4085
|
return payload;
|
|
4068
4086
|
}
|
|
4087
|
+
/**
|
|
4088
|
+
* Devuelve un lote fallido al buffer, delante de lo que haya llegado mientras se enviaba
|
|
4089
|
+
* (orden cronológico). Con el buffer lleno se descartan los eventos más antiguos.
|
|
4090
|
+
*/
|
|
4091
|
+
requeue(batch) {
|
|
4092
|
+
this.events = [...batch, ...this.events];
|
|
4093
|
+
this.enforceBufferCap();
|
|
4094
|
+
}
|
|
4095
|
+
/** Techo del buffer: si el envío lleva rato fallando, se sacrifica lo más antiguo. */
|
|
4096
|
+
enforceBufferCap() {
|
|
4097
|
+
const overflow = this.events.length - this.maxBufferedEvents;
|
|
4098
|
+
if (overflow > 0) this.events.splice(0, overflow);
|
|
4099
|
+
}
|
|
4069
4100
|
};
|
|
4070
4101
|
|
|
4071
4102
|
// src/analytics/feedback-payload.ts
|
|
@@ -4128,33 +4159,80 @@ function buildAnalyticsFeedbackBody(envelope, keys, feedbackSessionId) {
|
|
|
4128
4159
|
...feedbackSessionId ? { sessionId: feedbackSessionId } : {}
|
|
4129
4160
|
};
|
|
4130
4161
|
}
|
|
4162
|
+
var KEEPALIVE_MAX_BYTES = 6e4;
|
|
4163
|
+
function isRetryableStatus(status) {
|
|
4164
|
+
return status >= 500 || status === 408 || status === 429;
|
|
4165
|
+
}
|
|
4166
|
+
async function safeBodyText(res) {
|
|
4167
|
+
try {
|
|
4168
|
+
return (await res.text()).slice(0, 500);
|
|
4169
|
+
} catch {
|
|
4170
|
+
return "";
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4131
4173
|
function createFeedbackSink(options) {
|
|
4132
4174
|
let feedbackSessionId;
|
|
4133
|
-
|
|
4134
|
-
|
|
4175
|
+
let firstPostInFlight = null;
|
|
4176
|
+
const url = `${options.baseUrl}/sdk/feedback`;
|
|
4177
|
+
const post = async (body, final) => {
|
|
4178
|
+
const json = JSON.stringify(body);
|
|
4179
|
+
const small = json.length <= KEEPALIVE_MAX_BYTES;
|
|
4180
|
+
if (final && options.sendBeaconImpl && small) {
|
|
4181
|
+
const payload = typeof Blob !== "undefined" ? new Blob([json], { type: "application/json" }) : json;
|
|
4182
|
+
if (options.sendBeaconImpl(url, payload)) {
|
|
4183
|
+
options.log?.("[DeepdotsAnalytics] flush final v\xEDa sendBeacon");
|
|
4184
|
+
return;
|
|
4185
|
+
}
|
|
4186
|
+
options.log?.("[DeepdotsAnalytics] sendBeacon rechaz\xF3 el lote; fallback a fetch");
|
|
4187
|
+
}
|
|
4135
4188
|
const f = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
|
|
4136
4189
|
if (!f) {
|
|
4137
4190
|
options.log?.("[DeepdotsAnalytics] no fetch disponible; payload no enviado", body);
|
|
4138
4191
|
return;
|
|
4139
4192
|
}
|
|
4140
|
-
|
|
4141
|
-
f(`${options.baseUrl}/sdk/feedback`, {
|
|
4193
|
+
const res = await f(url, {
|
|
4142
4194
|
method: "POST",
|
|
4143
4195
|
headers: { "Content-Type": "application/json" },
|
|
4144
|
-
body:
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
}
|
|
4155
|
-
|
|
4196
|
+
body: json,
|
|
4197
|
+
// keepalive: el POST sobrevive a la navegación mientras el body sea pequeño.
|
|
4198
|
+
...small ? { keepalive: true } : {}
|
|
4199
|
+
});
|
|
4200
|
+
if (!res.ok) {
|
|
4201
|
+
const detail = await safeBodyText(res);
|
|
4202
|
+
if (isRetryableStatus(res.status)) {
|
|
4203
|
+
throw new Error(`POST /sdk/feedback ${res.status} (reintentable): ${detail}`);
|
|
4204
|
+
}
|
|
4205
|
+
options.log?.(
|
|
4206
|
+
`[DeepdotsAnalytics] POST /sdk/feedback rechazado con ${res.status}; lote DESCARTADO:`,
|
|
4207
|
+
detail
|
|
4208
|
+
);
|
|
4209
|
+
return;
|
|
4210
|
+
}
|
|
4211
|
+
try {
|
|
4212
|
+
const data = await res.json();
|
|
4213
|
+
if (data?.sessionId && data.sessionId !== feedbackSessionId) {
|
|
4214
|
+
feedbackSessionId = data.sessionId;
|
|
4215
|
+
options.log?.("[DeepdotsAnalytics] feedbackSessionId cacheado:", feedbackSessionId);
|
|
4216
|
+
options.onSessionId?.(feedbackSessionId);
|
|
4156
4217
|
}
|
|
4157
|
-
}
|
|
4218
|
+
} catch {
|
|
4219
|
+
}
|
|
4220
|
+
};
|
|
4221
|
+
return async (envelope, meta) => {
|
|
4222
|
+
const final = meta?.final === true;
|
|
4223
|
+
if (!feedbackSessionId && firstPostInFlight && !final) {
|
|
4224
|
+
await firstPostInFlight;
|
|
4225
|
+
}
|
|
4226
|
+
const body = buildAnalyticsFeedbackBody(envelope, options.keys, feedbackSessionId);
|
|
4227
|
+
options.log?.("[DeepdotsAnalytics] POST /sdk/feedback \u2192", body);
|
|
4228
|
+
const sent = post(body, final);
|
|
4229
|
+
if (!feedbackSessionId) {
|
|
4230
|
+
firstPostInFlight = sent.then(
|
|
4231
|
+
() => void 0,
|
|
4232
|
+
() => void 0
|
|
4233
|
+
);
|
|
4234
|
+
}
|
|
4235
|
+
await sent;
|
|
4158
4236
|
};
|
|
4159
4237
|
}
|
|
4160
4238
|
|
|
@@ -4648,6 +4726,11 @@ var DeepdotsPopups = class {
|
|
|
4648
4726
|
this.contact = null;
|
|
4649
4727
|
/** Marca si ya se inició la navegación manual (setScreen) — para RN sin History API. */
|
|
4650
4728
|
this.navStarted = false;
|
|
4729
|
+
/**
|
|
4730
|
+
* Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
|
|
4731
|
+
* verdad para el context de analytics Y para la segmentación por idioma de los popups.
|
|
4732
|
+
*/
|
|
4733
|
+
this.language = void 0;
|
|
4651
4734
|
}
|
|
4652
4735
|
/** Initialize the SDK with configuration */
|
|
4653
4736
|
init(config) {
|
|
@@ -4689,19 +4772,22 @@ var DeepdotsPopups = class {
|
|
|
4689
4772
|
baseUrl: this.baseUrl,
|
|
4690
4773
|
keys: config.analytics,
|
|
4691
4774
|
log: (...a) => this.log(...a),
|
|
4775
|
+
// sendBeacon: único transporte que sobrevive al cierre de la página.
|
|
4776
|
+
sendBeaconImpl: typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" ? (url, body) => navigator.sendBeacon(url, body) : void 0,
|
|
4692
4777
|
onSessionId: (id) => {
|
|
4693
4778
|
this.analyticsFeedbackSessionId = id;
|
|
4694
4779
|
}
|
|
4695
4780
|
}) : createDryRunSink((...a) => this.logger.log(...a));
|
|
4696
4781
|
const device = config.device ?? collectDeviceInfo(config.appVersion);
|
|
4782
|
+
this.language = resolveLanguage({
|
|
4783
|
+
explicit: config.language,
|
|
4784
|
+
navigator: typeof navigator !== "undefined" ? navigator : void 0,
|
|
4785
|
+
intl: typeof Intl !== "undefined" ? Intl : void 0
|
|
4786
|
+
});
|
|
4697
4787
|
this.analytics = new AnalyticsManager({
|
|
4698
4788
|
sink: analyticsSink,
|
|
4699
4789
|
publicKey: config.analytics?.publicKey ?? this.config.apiKey,
|
|
4700
|
-
language:
|
|
4701
|
-
explicit: config.language,
|
|
4702
|
-
navigator: typeof navigator !== "undefined" ? navigator : void 0,
|
|
4703
|
-
intl: typeof Intl !== "undefined" ? Intl : void 0
|
|
4704
|
-
}),
|
|
4790
|
+
language: this.language,
|
|
4705
4791
|
platform: config.platform ?? "web",
|
|
4706
4792
|
device,
|
|
4707
4793
|
maxBatchSize: ANALYTICS_MAX_BATCH_SIZE,
|
|
@@ -4877,10 +4963,14 @@ var DeepdotsPopups = class {
|
|
|
4877
4963
|
events: []
|
|
4878
4964
|
};
|
|
4879
4965
|
}
|
|
4880
|
-
/**
|
|
4881
|
-
|
|
4966
|
+
/**
|
|
4967
|
+
* Envía el lote acumulado de analytics y vacía el buffer.
|
|
4968
|
+
* `final: true` (cierre de página/app) cambia el transporte a `sendBeacon`, que sobrevive
|
|
4969
|
+
* al unload; un lote que falle por red o 5xx se re-encola para el siguiente flush.
|
|
4970
|
+
*/
|
|
4971
|
+
flushAnalytics(options) {
|
|
4882
4972
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
4883
|
-
this.analytics?.flush(this.analyticsIdentity());
|
|
4973
|
+
this.analytics?.flush(this.analyticsIdentity(), options);
|
|
4884
4974
|
}
|
|
4885
4975
|
/** Emite un evento `user_engagement` con el tiempo activo acumulado (#8). */
|
|
4886
4976
|
flushEngagement() {
|
|
@@ -4903,7 +4993,7 @@ var DeepdotsPopups = class {
|
|
|
4903
4993
|
this.navObserver?.stop();
|
|
4904
4994
|
if (this.tracking?.isTrackingEnabled()) this.analytics?.exitAllMiniServices();
|
|
4905
4995
|
this.flushEngagement();
|
|
4906
|
-
this.flushAnalytics();
|
|
4996
|
+
this.flushAnalytics({ final: true });
|
|
4907
4997
|
});
|
|
4908
4998
|
document.addEventListener("visibilitychange", () => {
|
|
4909
4999
|
if (document.visibilityState === "hidden") {
|
|
@@ -5024,14 +5114,16 @@ var DeepdotsPopups = class {
|
|
|
5024
5114
|
matchesSegmentsLang(def) {
|
|
5025
5115
|
const langs = def.segments?.lang;
|
|
5026
5116
|
if (!langs || langs.length === 0) return true;
|
|
5027
|
-
const
|
|
5028
|
-
if (!
|
|
5029
|
-
this.debug("No
|
|
5117
|
+
const currentLang = (this.language ?? "").toLowerCase();
|
|
5118
|
+
if (!currentLang) {
|
|
5119
|
+
this.debug("No language available for lang comparison", { popupId: def.id, langs });
|
|
5030
5120
|
return true;
|
|
5031
5121
|
}
|
|
5032
|
-
const matches = langs.some(
|
|
5122
|
+
const matches = langs.some(
|
|
5123
|
+
(lang) => typeof lang === "string" && currentLang.startsWith(lang.toLowerCase())
|
|
5124
|
+
);
|
|
5033
5125
|
if (!matches) {
|
|
5034
|
-
this.debug("Lang mismatch for popup", { popupId: def.id, langs,
|
|
5126
|
+
this.debug("Lang mismatch for popup", { popupId: def.id, langs, currentLang });
|
|
5035
5127
|
}
|
|
5036
5128
|
return matches;
|
|
5037
5129
|
}
|