@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/index.js
CHANGED
|
@@ -3077,6 +3077,11 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
|
|
|
3077
3077
|
const isDark = style?.theme === "dark";
|
|
3078
3078
|
const theme = {
|
|
3079
3079
|
popupBg: isDark ? "#1e1e1e" : "#fff",
|
|
3080
|
+
// El popup fija su propio fondo, así que debe fijar también el esquema de color.
|
|
3081
|
+
// Sin esto los controles de formulario (input/textarea/select) caen al estilo por
|
|
3082
|
+
// defecto del navegador: en un host con `prefers-color-scheme: dark` se pintan
|
|
3083
|
+
// oscuros con texto blanco sobre el fondo claro del popup.
|
|
3084
|
+
colorScheme: isDark ? "dark" : "light",
|
|
3080
3085
|
textPrimary: isDark ? "#f0f0f0" : "#111",
|
|
3081
3086
|
closeBtnHoverBg: isDark ? "rgba(255,255,255,0.10)" : "rgba(0,0,0,0.06)",
|
|
3082
3087
|
closeBtnHoverColor: isDark ? "#fff" : "#000000"
|
|
@@ -3099,6 +3104,7 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
|
|
|
3099
3104
|
flex-direction: column;
|
|
3100
3105
|
justify-content: flex-start;
|
|
3101
3106
|
background: ${theme.popupBg};
|
|
3107
|
+
color-scheme: ${theme.colorScheme};
|
|
3102
3108
|
border-radius: 8px;
|
|
3103
3109
|
padding: 24px;
|
|
3104
3110
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
@@ -3550,6 +3556,9 @@ __export(index_exports, {
|
|
|
3550
3556
|
CrashReporter: () => CrashReporter,
|
|
3551
3557
|
DeepdotsPopups: () => DeepdotsPopups,
|
|
3552
3558
|
InMemoryStorage: () => InMemoryStorage,
|
|
3559
|
+
MAX_TRACKED_MESSAGES: () => MAX_TRACKED_MESSAGES,
|
|
3560
|
+
MESSAGE_CHANNELS: () => MESSAGE_CHANNELS,
|
|
3561
|
+
MessageGuard: () => MessageGuard,
|
|
3553
3562
|
NoopPopupRenderer: () => NoopPopupRenderer,
|
|
3554
3563
|
POPUP_TRIGGER_CONDITION_STATUSES: () => POPUP_TRIGGER_CONDITION_STATUSES,
|
|
3555
3564
|
ReactNativePopupRenderer: () => ReactNativePopupRenderer,
|
|
@@ -3968,6 +3977,7 @@ var AnalyticsManager = class {
|
|
|
3968
3977
|
this.device = options.device;
|
|
3969
3978
|
this.platform = options.platform ?? "web";
|
|
3970
3979
|
this.maxBatchSize = options.maxBatchSize ?? 20;
|
|
3980
|
+
this.maxBufferedEvents = options.maxBufferedEvents ?? 200;
|
|
3971
3981
|
this.onFlushNeeded = options.onFlushNeeded;
|
|
3972
3982
|
}
|
|
3973
3983
|
/** Actualiza campos de device en runtime (p.ej. country/city tras resolver geo). */
|
|
@@ -4016,6 +4026,15 @@ var AnalyticsManager = class {
|
|
|
4016
4026
|
this.exitMiniService(name);
|
|
4017
4027
|
}
|
|
4018
4028
|
}
|
|
4029
|
+
/**
|
|
4030
|
+
* Olvida lo que pertenecía al usuario anterior (user attributes + métricas) al cambiar de
|
|
4031
|
+
* usuario. NO toca el buffer de eventos: el cierre de sesión ya lo vació con la identidad
|
|
4032
|
+
* vieja, y lo que llegue después es del usuario nuevo.
|
|
4033
|
+
*/
|
|
4034
|
+
resetUserScope() {
|
|
4035
|
+
this.attributes = {};
|
|
4036
|
+
this.metrics = {};
|
|
4037
|
+
}
|
|
4019
4038
|
/** Registra un evento de analítica (modelo GA: nombre + params). */
|
|
4020
4039
|
track(name, params) {
|
|
4021
4040
|
const current = this.getMiniService();
|
|
@@ -4028,6 +4047,7 @@ var AnalyticsManager = class {
|
|
|
4028
4047
|
timestamp: this.now(),
|
|
4029
4048
|
params: Object.keys(merged).length ? merged : void 0
|
|
4030
4049
|
});
|
|
4050
|
+
this.enforceBufferCap();
|
|
4031
4051
|
if (this.events.length >= this.maxBatchSize) {
|
|
4032
4052
|
this.onFlushNeeded?.();
|
|
4033
4053
|
}
|
|
@@ -4058,14 +4078,43 @@ var AnalyticsManager = class {
|
|
|
4058
4078
|
events: [...this.events]
|
|
4059
4079
|
};
|
|
4060
4080
|
}
|
|
4061
|
-
/**
|
|
4062
|
-
|
|
4081
|
+
/**
|
|
4082
|
+
* Envía (vía sink) el lote acumulado y vacía el buffer. No-op si no hay eventos.
|
|
4083
|
+
*
|
|
4084
|
+
* Si el sink falla de forma transitoria (promesa rechazada o excepción síncrona) el lote
|
|
4085
|
+
* se **re-encola** al principio del buffer y se reintenta en el siguiente flush: un error
|
|
4086
|
+
* de red o un 5xx no debe hacer desaparecer los eventos.
|
|
4087
|
+
*/
|
|
4088
|
+
flush(identity, meta) {
|
|
4063
4089
|
if (this.events.length === 0) return null;
|
|
4090
|
+
const batch = this.events;
|
|
4064
4091
|
const payload = this.buildPayload(identity);
|
|
4065
|
-
this.sink(payload);
|
|
4066
4092
|
this.events = [];
|
|
4093
|
+
let result;
|
|
4094
|
+
try {
|
|
4095
|
+
result = meta ? this.sink(payload, meta) : this.sink(payload);
|
|
4096
|
+
} catch {
|
|
4097
|
+
this.requeue(batch);
|
|
4098
|
+
return payload;
|
|
4099
|
+
}
|
|
4100
|
+
if (result && typeof result.then === "function") {
|
|
4101
|
+
result.catch(() => this.requeue(batch));
|
|
4102
|
+
}
|
|
4067
4103
|
return payload;
|
|
4068
4104
|
}
|
|
4105
|
+
/**
|
|
4106
|
+
* Devuelve un lote fallido al buffer, delante de lo que haya llegado mientras se enviaba
|
|
4107
|
+
* (orden cronológico). Con el buffer lleno se descartan los eventos más antiguos.
|
|
4108
|
+
*/
|
|
4109
|
+
requeue(batch) {
|
|
4110
|
+
this.events = [...batch, ...this.events];
|
|
4111
|
+
this.enforceBufferCap();
|
|
4112
|
+
}
|
|
4113
|
+
/** Techo del buffer: si el envío lleva rato fallando, se sacrifica lo más antiguo. */
|
|
4114
|
+
enforceBufferCap() {
|
|
4115
|
+
const overflow = this.events.length - this.maxBufferedEvents;
|
|
4116
|
+
if (overflow > 0) this.events.splice(0, overflow);
|
|
4117
|
+
}
|
|
4069
4118
|
};
|
|
4070
4119
|
|
|
4071
4120
|
// src/analytics/feedback-payload.ts
|
|
@@ -4073,7 +4122,7 @@ function pushKV(list, key, value) {
|
|
|
4073
4122
|
if (value === void 0 || value === null || value === "") return;
|
|
4074
4123
|
list.push({ key, value: [String(value)] });
|
|
4075
4124
|
}
|
|
4076
|
-
function buildAnalyticsFeedbackBody(envelope, keys, feedbackSessionId) {
|
|
4125
|
+
function buildAnalyticsFeedbackBody(envelope, keys, feedbackSessionId, options) {
|
|
4077
4126
|
const { context } = envelope;
|
|
4078
4127
|
const profile = [];
|
|
4079
4128
|
pushKV(profile, "external-user-id", envelope.userId);
|
|
@@ -4124,37 +4173,91 @@ function buildAnalyticsFeedbackBody(envelope, keys, feedbackSessionId) {
|
|
|
4124
4173
|
},
|
|
4125
4174
|
publicKey: keys.publicKey,
|
|
4126
4175
|
integration: keys.integration,
|
|
4127
|
-
|
|
4176
|
+
// Único marcador de cierre acordado con backend: el último lote de la sesión.
|
|
4177
|
+
completed: options?.sessionEnd === true,
|
|
4128
4178
|
...feedbackSessionId ? { sessionId: feedbackSessionId } : {}
|
|
4129
4179
|
};
|
|
4130
4180
|
}
|
|
4181
|
+
var KEEPALIVE_MAX_BYTES = 6e4;
|
|
4182
|
+
function isRetryableStatus(status) {
|
|
4183
|
+
return status >= 500 || status === 408 || status === 429;
|
|
4184
|
+
}
|
|
4185
|
+
async function safeBodyText(res) {
|
|
4186
|
+
try {
|
|
4187
|
+
return (await res.text()).slice(0, 500);
|
|
4188
|
+
} catch {
|
|
4189
|
+
return "";
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4131
4192
|
function createFeedbackSink(options) {
|
|
4132
4193
|
let feedbackSessionId;
|
|
4133
|
-
|
|
4134
|
-
|
|
4194
|
+
let firstPostInFlight = null;
|
|
4195
|
+
const url = `${options.baseUrl}/sdk/feedback`;
|
|
4196
|
+
const post = async (body, final, sessionEnd = false) => {
|
|
4197
|
+
const json = JSON.stringify(body);
|
|
4198
|
+
const small = json.length <= KEEPALIVE_MAX_BYTES;
|
|
4199
|
+
if (final && options.sendBeaconImpl && small) {
|
|
4200
|
+
const payload = typeof Blob !== "undefined" ? new Blob([json], { type: "application/json" }) : json;
|
|
4201
|
+
if (options.sendBeaconImpl(url, payload)) {
|
|
4202
|
+
options.log?.("[DeepdotsAnalytics] flush final v\xEDa sendBeacon");
|
|
4203
|
+
return;
|
|
4204
|
+
}
|
|
4205
|
+
options.log?.("[DeepdotsAnalytics] sendBeacon rechaz\xF3 el lote; fallback a fetch");
|
|
4206
|
+
}
|
|
4135
4207
|
const f = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
|
|
4136
4208
|
if (!f) {
|
|
4137
4209
|
options.log?.("[DeepdotsAnalytics] no fetch disponible; payload no enviado", body);
|
|
4138
4210
|
return;
|
|
4139
4211
|
}
|
|
4140
|
-
|
|
4141
|
-
f(`${options.baseUrl}/sdk/feedback`, {
|
|
4212
|
+
const res = await f(url, {
|
|
4142
4213
|
method: "POST",
|
|
4143
4214
|
headers: { "Content-Type": "application/json" },
|
|
4144
|
-
body:
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
}
|
|
4155
|
-
|
|
4215
|
+
body: json,
|
|
4216
|
+
// keepalive: el POST sobrevive a la navegación mientras el body sea pequeño.
|
|
4217
|
+
...small ? { keepalive: true } : {}
|
|
4218
|
+
});
|
|
4219
|
+
if (!res.ok) {
|
|
4220
|
+
const detail = await safeBodyText(res);
|
|
4221
|
+
if (isRetryableStatus(res.status)) {
|
|
4222
|
+
throw new Error(`POST /sdk/feedback ${res.status} (reintentable): ${detail}`);
|
|
4223
|
+
}
|
|
4224
|
+
options.log?.(
|
|
4225
|
+
`[DeepdotsAnalytics] POST /sdk/feedback rechazado con ${res.status}; lote DESCARTADO:`,
|
|
4226
|
+
detail
|
|
4227
|
+
);
|
|
4228
|
+
return;
|
|
4229
|
+
}
|
|
4230
|
+
if (sessionEnd) return;
|
|
4231
|
+
try {
|
|
4232
|
+
const data = await res.json();
|
|
4233
|
+
if (data?.sessionId && data.sessionId !== feedbackSessionId) {
|
|
4234
|
+
feedbackSessionId = data.sessionId;
|
|
4235
|
+
options.log?.("[DeepdotsAnalytics] feedbackSessionId cacheado:", feedbackSessionId);
|
|
4236
|
+
options.onSessionId?.(feedbackSessionId);
|
|
4156
4237
|
}
|
|
4157
|
-
}
|
|
4238
|
+
} catch {
|
|
4239
|
+
}
|
|
4240
|
+
};
|
|
4241
|
+
return async (envelope, meta) => {
|
|
4242
|
+
const final = meta?.final === true;
|
|
4243
|
+
const sessionEnd = meta?.sessionEnd === true;
|
|
4244
|
+
if (!feedbackSessionId && firstPostInFlight && !final) {
|
|
4245
|
+
await firstPostInFlight;
|
|
4246
|
+
}
|
|
4247
|
+
const body = buildAnalyticsFeedbackBody(envelope, options.keys, feedbackSessionId, { sessionEnd });
|
|
4248
|
+
options.log?.("[DeepdotsAnalytics] POST /sdk/feedback \u2192", body);
|
|
4249
|
+
const sent = post(body, final, sessionEnd);
|
|
4250
|
+
if (sessionEnd) {
|
|
4251
|
+
feedbackSessionId = void 0;
|
|
4252
|
+
firstPostInFlight = null;
|
|
4253
|
+
options.onSessionReset?.();
|
|
4254
|
+
} else if (!feedbackSessionId) {
|
|
4255
|
+
firstPostInFlight = sent.then(
|
|
4256
|
+
() => void 0,
|
|
4257
|
+
() => void 0
|
|
4258
|
+
);
|
|
4259
|
+
}
|
|
4260
|
+
await sent;
|
|
4158
4261
|
};
|
|
4159
4262
|
}
|
|
4160
4263
|
|
|
@@ -4587,6 +4690,7 @@ var CrashReporter = class {
|
|
|
4587
4690
|
init_logger();
|
|
4588
4691
|
|
|
4589
4692
|
// src/analytics/messaging.ts
|
|
4693
|
+
var MESSAGE_CHANNELS = ["push", "in_app"];
|
|
4590
4694
|
function buildMessageParams(stage, o) {
|
|
4591
4695
|
const p = {
|
|
4592
4696
|
stage,
|
|
@@ -4599,6 +4703,57 @@ function buildMessageParams(stage, o) {
|
|
|
4599
4703
|
if (o.currency) p.currency = o.currency;
|
|
4600
4704
|
return { ...p, ...o.params ?? {} };
|
|
4601
4705
|
}
|
|
4706
|
+
var MAX_TRACKED_MESSAGES = 500;
|
|
4707
|
+
var MessageGuard = class {
|
|
4708
|
+
constructor(maxTrackedMessages = MAX_TRACKED_MESSAGES) {
|
|
4709
|
+
this.maxTrackedMessages = maxTrackedMessages;
|
|
4710
|
+
/** message_id → canal fijado + stages ya emitidos. El orden de inserción marca la eviction. */
|
|
4711
|
+
this.seen = /* @__PURE__ */ new Map();
|
|
4712
|
+
}
|
|
4713
|
+
evaluate(stage, options) {
|
|
4714
|
+
const channel = options.channel;
|
|
4715
|
+
if (!MESSAGE_CHANNELS.includes(channel)) {
|
|
4716
|
+
return {
|
|
4717
|
+
emit: false,
|
|
4718
|
+
reason: "invalid_channel",
|
|
4719
|
+
detail: `channel "${channel}" no v\xE1lido (esperado ${MESSAGE_CHANNELS.join(" | ")})`
|
|
4720
|
+
};
|
|
4721
|
+
}
|
|
4722
|
+
const entry = this.seen.get(options.id);
|
|
4723
|
+
if (!entry) {
|
|
4724
|
+
this.remember(options.id, channel, stage);
|
|
4725
|
+
return { emit: true };
|
|
4726
|
+
}
|
|
4727
|
+
if (entry.channel !== channel) {
|
|
4728
|
+
return {
|
|
4729
|
+
emit: false,
|
|
4730
|
+
reason: "channel_conflict",
|
|
4731
|
+
detail: `message_id "${options.id}" ya se report\xF3 en channel "${entry.channel}"; se descarta "${channel}"`
|
|
4732
|
+
};
|
|
4733
|
+
}
|
|
4734
|
+
if (entry.stages.has(stage)) {
|
|
4735
|
+
return {
|
|
4736
|
+
emit: false,
|
|
4737
|
+
reason: "duplicate_stage",
|
|
4738
|
+
detail: `stage "${stage}" ya emitido para message_id "${options.id}"`
|
|
4739
|
+
};
|
|
4740
|
+
}
|
|
4741
|
+
entry.stages.add(stage);
|
|
4742
|
+
return { emit: true };
|
|
4743
|
+
}
|
|
4744
|
+
/** Olvida todo lo vigilado (nueva sesión). */
|
|
4745
|
+
reset() {
|
|
4746
|
+
this.seen.clear();
|
|
4747
|
+
}
|
|
4748
|
+
remember(id, channel, stage) {
|
|
4749
|
+
this.seen.set(id, { channel, stages: /* @__PURE__ */ new Set([stage]) });
|
|
4750
|
+
while (this.seen.size > this.maxTrackedMessages) {
|
|
4751
|
+
const oldest = this.seen.keys().next();
|
|
4752
|
+
if (oldest.done) break;
|
|
4753
|
+
this.seen.delete(oldest.value);
|
|
4754
|
+
}
|
|
4755
|
+
}
|
|
4756
|
+
};
|
|
4602
4757
|
|
|
4603
4758
|
// src/core/deepdots-popups.ts
|
|
4604
4759
|
var EXIT_QUEUE_STORAGE_KEY = "__deepdots_exit_popup_queue__";
|
|
@@ -4640,20 +4795,40 @@ var DeepdotsPopups = class {
|
|
|
4640
4795
|
this.analyticsFlushTimer = void 0;
|
|
4641
4796
|
/** Crash & error reporting (#14–17). Null hasta init(). */
|
|
4642
4797
|
this.crashReporter = null;
|
|
4798
|
+
/** Protecciones del funnel de Messaging (#18–22). Vigencia de sesión: se reinicia en init(). */
|
|
4799
|
+
this.messageGuard = new MessageGuard();
|
|
4643
4800
|
/** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
|
|
4644
4801
|
this.navObserver = null;
|
|
4645
4802
|
/** Tiempo activo (engagement time, #8). */
|
|
4646
4803
|
this.engagement = null;
|
|
4647
4804
|
/** Atributos internos del usuario identificado → POST /sdk/popups/contact. Null si no hay userId. */
|
|
4648
4805
|
this.contact = null;
|
|
4806
|
+
/** Storage resuelto en init() (inyectado por el host o localStorage). Reusado al cambiar de usuario. */
|
|
4807
|
+
this.storage = null;
|
|
4649
4808
|
/** Marca si ya se inició la navegación manual (setScreen) — para RN sin History API. */
|
|
4650
4809
|
this.navStarted = false;
|
|
4810
|
+
/**
|
|
4811
|
+
* `true` entre `session_start` y `session_end`. Hace idempotente tanto la apertura como
|
|
4812
|
+
* el cierre (p. ej. `visibilitychange` + `pagehide` seguidos) y permite abrir una sesión
|
|
4813
|
+
* nueva al volver a foreground o al conceder el consentimiento más tarde.
|
|
4814
|
+
*/
|
|
4815
|
+
this.sessionOpen = false;
|
|
4816
|
+
/**
|
|
4817
|
+
* Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
|
|
4818
|
+
* verdad para el context de analytics Y para la segmentación por idioma de los popups.
|
|
4819
|
+
*/
|
|
4820
|
+
this.language = void 0;
|
|
4651
4821
|
}
|
|
4652
4822
|
/** Initialize the SDK with configuration */
|
|
4653
4823
|
init(config) {
|
|
4654
4824
|
this.logger = config.logger ?? console;
|
|
4655
4825
|
setLogger(this.logger);
|
|
4656
4826
|
if (this.initialized) {
|
|
4827
|
+
const nextUserId = config.userId || void 0;
|
|
4828
|
+
if (nextUserId !== this.config?.userId) {
|
|
4829
|
+
this.setUserId(nextUserId);
|
|
4830
|
+
return;
|
|
4831
|
+
}
|
|
4657
4832
|
this.log("SDK already initialized");
|
|
4658
4833
|
return;
|
|
4659
4834
|
}
|
|
@@ -4666,6 +4841,7 @@ var DeepdotsPopups = class {
|
|
|
4666
4841
|
userId: config.userId || void 0
|
|
4667
4842
|
};
|
|
4668
4843
|
const storage = config.storage ?? createDefaultStorage();
|
|
4844
|
+
this.storage = storage;
|
|
4669
4845
|
this.tracking = new TrackingManager({
|
|
4670
4846
|
storage,
|
|
4671
4847
|
clientUserId: this.config.userId,
|
|
@@ -4689,19 +4865,26 @@ var DeepdotsPopups = class {
|
|
|
4689
4865
|
baseUrl: this.baseUrl,
|
|
4690
4866
|
keys: config.analytics,
|
|
4691
4867
|
log: (...a) => this.log(...a),
|
|
4868
|
+
// sendBeacon: único transporte que sobrevive al cierre de la página.
|
|
4869
|
+
sendBeaconImpl: typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" ? (url, body) => navigator.sendBeacon(url, body) : void 0,
|
|
4692
4870
|
onSessionId: (id) => {
|
|
4693
4871
|
this.analyticsFeedbackSessionId = id;
|
|
4872
|
+
},
|
|
4873
|
+
onSessionReset: () => {
|
|
4874
|
+
this.analyticsFeedbackSessionId = void 0;
|
|
4694
4875
|
}
|
|
4695
4876
|
}) : createDryRunSink((...a) => this.logger.log(...a));
|
|
4696
4877
|
const device = config.device ?? collectDeviceInfo(config.appVersion);
|
|
4878
|
+
this.language = resolveLanguage({
|
|
4879
|
+
explicit: config.language,
|
|
4880
|
+
navigator: typeof navigator !== "undefined" ? navigator : void 0,
|
|
4881
|
+
intl: typeof Intl !== "undefined" ? Intl : void 0
|
|
4882
|
+
});
|
|
4883
|
+
this.messageGuard.reset();
|
|
4697
4884
|
this.analytics = new AnalyticsManager({
|
|
4698
4885
|
sink: analyticsSink,
|
|
4699
4886
|
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
|
-
}),
|
|
4887
|
+
language: this.language,
|
|
4705
4888
|
platform: config.platform ?? "web",
|
|
4706
4889
|
device,
|
|
4707
4890
|
maxBatchSize: ANALYTICS_MAX_BATCH_SIZE,
|
|
@@ -4733,7 +4916,7 @@ var DeepdotsPopups = class {
|
|
|
4733
4916
|
this.engagement = new EngagementTracker();
|
|
4734
4917
|
this.engagement.resume();
|
|
4735
4918
|
this.setupAnalyticsFlush();
|
|
4736
|
-
this.
|
|
4919
|
+
this.openSession();
|
|
4737
4920
|
const pendingCrashes = this.crashReporter.drainPendingCrashes();
|
|
4738
4921
|
if (this.tracking?.isTrackingEnabled()) {
|
|
4739
4922
|
for (const rec of pendingCrashes) {
|
|
@@ -4765,7 +4948,9 @@ var DeepdotsPopups = class {
|
|
|
4765
4948
|
}
|
|
4766
4949
|
/** Activa/desactiva el tracking (identidad + sesión + analytics). Kill-switch del contrato §7bis. */
|
|
4767
4950
|
setTrackingEnabled(enabled) {
|
|
4951
|
+
if (!enabled) this.closeSession("tracking_disabled");
|
|
4768
4952
|
this.tracking?.setTrackingEnabled(enabled);
|
|
4953
|
+
if (enabled) this.openSession();
|
|
4769
4954
|
this.log("tracking \xB7 setTrackingEnabled:", enabled, "\xB7 session_id:", this.tracking?.getSessionId() ?? null);
|
|
4770
4955
|
}
|
|
4771
4956
|
// ───────── Analytics (canal separado del feedback, vinculado por user_id) ─────────
|
|
@@ -4835,8 +5020,18 @@ var DeepdotsPopups = class {
|
|
|
4835
5020
|
trackFunnelStep(funnel, step, taskId, params) {
|
|
4836
5021
|
this.track("deepdots_funnel_step", { funnel, step, task_id: taskId, ...params ?? {} });
|
|
4837
5022
|
}
|
|
4838
|
-
/**
|
|
5023
|
+
/**
|
|
5024
|
+
* Messaging (#18–22): registra una etapa del funnel de una notificación (push/in-app).
|
|
5025
|
+
* No-op si tracking off. El evento se descarta (con warning) si el `channel` no es válido,
|
|
5026
|
+
* si ese `(message_id, stage)` ya se emitió en la sesión, o si el `message_id` ya se reportó
|
|
5027
|
+
* en otro canal — ver `MessageGuard`.
|
|
5028
|
+
*/
|
|
4839
5029
|
trackMessage(stage, options) {
|
|
5030
|
+
const verdict = this.messageGuard.evaluate(stage, options);
|
|
5031
|
+
if (!verdict.emit) {
|
|
5032
|
+
console.warn(`[DeepdotsPopups] trackMessage descartado (${verdict.reason}): ${verdict.detail}`);
|
|
5033
|
+
return;
|
|
5034
|
+
}
|
|
4840
5035
|
this.track("deepdots_message", buildMessageParams(stage, options));
|
|
4841
5036
|
}
|
|
4842
5037
|
/**
|
|
@@ -4852,21 +5047,86 @@ var DeepdotsPopups = class {
|
|
|
4852
5047
|
this.navObserver.visit(name);
|
|
4853
5048
|
}
|
|
4854
5049
|
}
|
|
4855
|
-
/** App a foreground (RN: AppState 'active'): reanuda el engagement time. */
|
|
5050
|
+
/** App a foreground (RN: AppState 'active'): reanuda el engagement time y abre sesión nueva. */
|
|
4856
5051
|
onForeground() {
|
|
4857
5052
|
this.engagement?.resume();
|
|
5053
|
+
this.openSession();
|
|
4858
5054
|
}
|
|
4859
5055
|
/**
|
|
4860
|
-
* App a background (RN: AppState 'background'):
|
|
4861
|
-
* mini-
|
|
5056
|
+
* App a background (RN: AppState 'background'): FIN DE SESIÓN. Cierra pantalla actual
|
|
5057
|
+
* (page_view), mini-services (mini_service_exit), emite engagement + `session_end` y
|
|
5058
|
+
* envía el último lote con `completed:true`.
|
|
5059
|
+
*
|
|
5060
|
+
* Es la única señal disponible en móvil: el kill de la app (swipe) NO da callback.
|
|
4862
5061
|
*/
|
|
4863
5062
|
onBackground() {
|
|
5063
|
+
this.closeSession("background");
|
|
5064
|
+
this.engagement?.pause();
|
|
5065
|
+
}
|
|
5066
|
+
/**
|
|
5067
|
+
* Cierre EXPLÍCITO de la sesión por parte del host (logout, fin de flujo…): emite
|
|
5068
|
+
* `session_end` y envía el último lote con `completed:true`.
|
|
5069
|
+
*/
|
|
5070
|
+
endSession() {
|
|
5071
|
+
this.closeSession("manual");
|
|
5072
|
+
}
|
|
5073
|
+
/**
|
|
5074
|
+
* Cambio de usuario (login / logout / cambio de cuenta). Cierra la sesión del usuario
|
|
5075
|
+
* anterior (`session_end` con `reason: 'user_change'` + `completed:true`), cambia la
|
|
5076
|
+
* identidad y abre una sesión nueva. Sin `userId` vuelve al id anónimo del SDK.
|
|
5077
|
+
*
|
|
5078
|
+
* Equivale a llamar a `init()` otra vez con otro `userId`.
|
|
5079
|
+
*/
|
|
5080
|
+
setUserId(userId) {
|
|
5081
|
+
const next = userId || void 0;
|
|
5082
|
+
if (!this.initialized || !this.config || next === this.config.userId) return;
|
|
5083
|
+
this.closeSession("user_change");
|
|
5084
|
+
const enabled = this.tracking?.isTrackingEnabled() ?? true;
|
|
5085
|
+
const storage = this.storage ?? createDefaultStorage();
|
|
5086
|
+
this.config.userId = next;
|
|
5087
|
+
this.tracking = new TrackingManager({ storage, clientUserId: next, enabled });
|
|
5088
|
+
this.contact = next ? new ContactManager({
|
|
5089
|
+
storage,
|
|
5090
|
+
publicKey: this.config.apiKey ?? "",
|
|
5091
|
+
userId: next,
|
|
5092
|
+
post: (body) => this.postContact(body)
|
|
5093
|
+
}) : null;
|
|
5094
|
+
this.analytics?.resetUserScope();
|
|
5095
|
+
this.messageGuard.reset();
|
|
5096
|
+
this.log("tracking \xB7 user_change \xB7 user_id:", this.tracking.getUserId());
|
|
5097
|
+
this.openSession();
|
|
5098
|
+
}
|
|
5099
|
+
/**
|
|
5100
|
+
* Cierra la sesión actual: vuelca todo lo que quedaba abierto y envía el último lote
|
|
5101
|
+
* marcado con `completed:true`, para que el backend cierre el registro. El `sessionId`
|
|
5102
|
+
* cacheado se olvida → el lote siguiente abre un registro nuevo.
|
|
5103
|
+
*
|
|
5104
|
+
* Idempotente: dos cierres seguidos (p. ej. `visibilitychange` + `pagehide`) no duplican
|
|
5105
|
+
* el `session_end`.
|
|
5106
|
+
*/
|
|
5107
|
+
closeSession(reason) {
|
|
5108
|
+
if (!this.sessionOpen) return;
|
|
5109
|
+
if (!this.tracking?.isTrackingEnabled()) return;
|
|
5110
|
+
this.sessionOpen = false;
|
|
4864
5111
|
this.navObserver?.stop();
|
|
4865
5112
|
this.navStarted = false;
|
|
4866
|
-
|
|
5113
|
+
this.analytics?.exitAllMiniServices();
|
|
4867
5114
|
this.flushEngagement();
|
|
4868
|
-
this.
|
|
4869
|
-
this.flushAnalytics();
|
|
5115
|
+
this.track("deepdots_session_end", { reason });
|
|
5116
|
+
this.flushAnalytics({ final: reason === "page_hide", sessionEnd: true });
|
|
5117
|
+
this.analyticsFeedbackSessionId = void 0;
|
|
5118
|
+
this.tracking?.setSessionId(null);
|
|
5119
|
+
this.log("tracking \xB7 session_end:", reason);
|
|
5120
|
+
}
|
|
5121
|
+
/**
|
|
5122
|
+
* Abre sesión (`session_start`) si no hay una abierta y el tracking está activo. Idempotente:
|
|
5123
|
+
* se llama en init(), al volver a foreground y al conceder el consentimiento.
|
|
5124
|
+
*/
|
|
5125
|
+
openSession() {
|
|
5126
|
+
if (this.sessionOpen) return;
|
|
5127
|
+
if (!this.tracking?.isTrackingEnabled()) return;
|
|
5128
|
+
this.sessionOpen = true;
|
|
5129
|
+
this.track("deepdots_session_start", {});
|
|
4870
5130
|
}
|
|
4871
5131
|
/** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
|
|
4872
5132
|
previewAnalytics() {
|
|
@@ -4877,10 +5137,14 @@ var DeepdotsPopups = class {
|
|
|
4877
5137
|
events: []
|
|
4878
5138
|
};
|
|
4879
5139
|
}
|
|
4880
|
-
/**
|
|
4881
|
-
|
|
5140
|
+
/**
|
|
5141
|
+
* Envía el lote acumulado de analytics y vacía el buffer.
|
|
5142
|
+
* `final: true` (cierre de página/app) cambia el transporte a `sendBeacon`, que sobrevive
|
|
5143
|
+
* al unload; un lote que falle por red o 5xx se re-encola para el siguiente flush.
|
|
5144
|
+
*/
|
|
5145
|
+
flushAnalytics(options) {
|
|
4882
5146
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
4883
|
-
this.analytics?.flush(this.analyticsIdentity());
|
|
5147
|
+
this.analytics?.flush(this.analyticsIdentity(), options);
|
|
4884
5148
|
}
|
|
4885
5149
|
/** Emite un evento `user_engagement` con el tiempo activo acumulado (#8). */
|
|
4886
5150
|
flushEngagement() {
|
|
@@ -4900,10 +5164,7 @@ var DeepdotsPopups = class {
|
|
|
4900
5164
|
this.analyticsFlushTimer = setInterval(() => this.flushAnalytics(), ANALYTICS_FLUSH_INTERVAL_MS);
|
|
4901
5165
|
window.addEventListener("pagehide", () => {
|
|
4902
5166
|
clearInterval(this.analyticsFlushTimer);
|
|
4903
|
-
this.
|
|
4904
|
-
if (this.tracking?.isTrackingEnabled()) this.analytics?.exitAllMiniServices();
|
|
4905
|
-
this.flushEngagement();
|
|
4906
|
-
this.flushAnalytics();
|
|
5167
|
+
this.closeSession("page_hide");
|
|
4907
5168
|
});
|
|
4908
5169
|
document.addEventListener("visibilitychange", () => {
|
|
4909
5170
|
if (document.visibilityState === "hidden") {
|
|
@@ -5024,14 +5285,16 @@ var DeepdotsPopups = class {
|
|
|
5024
5285
|
matchesSegmentsLang(def) {
|
|
5025
5286
|
const langs = def.segments?.lang;
|
|
5026
5287
|
if (!langs || langs.length === 0) return true;
|
|
5027
|
-
const
|
|
5028
|
-
if (!
|
|
5029
|
-
this.debug("No
|
|
5288
|
+
const currentLang = (this.language ?? "").toLowerCase();
|
|
5289
|
+
if (!currentLang) {
|
|
5290
|
+
this.debug("No language available for lang comparison", { popupId: def.id, langs });
|
|
5030
5291
|
return true;
|
|
5031
5292
|
}
|
|
5032
|
-
const matches = langs.some(
|
|
5293
|
+
const matches = langs.some(
|
|
5294
|
+
(lang) => typeof lang === "string" && currentLang.startsWith(lang.toLowerCase())
|
|
5295
|
+
);
|
|
5033
5296
|
if (!matches) {
|
|
5034
|
-
this.debug("Lang mismatch for popup", { popupId: def.id, langs,
|
|
5297
|
+
this.debug("Lang mismatch for popup", { popupId: def.id, langs, currentLang });
|
|
5035
5298
|
}
|
|
5036
5299
|
return matches;
|
|
5037
5300
|
}
|
|
@@ -5579,7 +5842,8 @@ function setupReactNative(sdk, config, deps = {}) {
|
|
|
5579
5842
|
if (deps.appState?.addEventListener) {
|
|
5580
5843
|
subscription = deps.appState.addEventListener("change", (state) => {
|
|
5581
5844
|
if (state === "active") sdk.onForeground();
|
|
5582
|
-
else sdk.onBackground();
|
|
5845
|
+
else if (state === "background") sdk.onBackground();
|
|
5846
|
+
else sdk.flushAnalytics();
|
|
5583
5847
|
});
|
|
5584
5848
|
}
|
|
5585
5849
|
return () => {
|