@magicfeedback/popup-sdk 1.1.0 → 1.1.2

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/dist/index.js CHANGED
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
8
11
  var __commonJS = (cb, mod) => function __require() {
9
12
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
13
  };
@@ -30,6 +33,117 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
33
  ));
31
34
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
35
 
36
+ // src/tracking/tracking-manager.ts
37
+ function createDefaultStorage() {
38
+ try {
39
+ if (typeof window !== "undefined" && window.localStorage) {
40
+ const ls = window.localStorage;
41
+ const probe = "__deepdots_probe__";
42
+ ls.setItem(probe, "1");
43
+ ls.removeItem(probe);
44
+ return {
45
+ getItem: (k) => ls.getItem(k),
46
+ setItem: (k, v) => ls.setItem(k, v),
47
+ removeItem: (k) => ls.removeItem(k)
48
+ };
49
+ }
50
+ } catch {
51
+ }
52
+ return new InMemoryStorage();
53
+ }
54
+ function buildSurveyIdentity(userId, sessionId, miniService = null, analyticsFeedbackSessionId = null) {
55
+ const profile = userId ? [{ key: "external-user-id", value: [userId] }] : [];
56
+ const metadata = [];
57
+ if (sessionId) metadata.push({ key: "session_id", value: [sessionId] });
58
+ if (userId) metadata.push({ key: "user_id", value: [userId] });
59
+ if (miniService) metadata.push({ key: "mini_service", value: [miniService] });
60
+ if (analyticsFeedbackSessionId) metadata.push({ key: "deepdots_analytics_feedback_session_id", value: [analyticsFeedbackSessionId] });
61
+ return { profile, metadata };
62
+ }
63
+ function defaultUuid() {
64
+ const g = typeof globalThis !== "undefined" ? globalThis : {};
65
+ if (g.crypto?.randomUUID) return g.crypto.randomUUID();
66
+ const bytes = new Uint8Array(16);
67
+ if (g.crypto?.getRandomValues) {
68
+ g.crypto.getRandomValues(bytes);
69
+ } else {
70
+ for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
71
+ }
72
+ bytes[6] = bytes[6] & 15 | 64;
73
+ bytes[8] = bytes[8] & 63 | 128;
74
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0"));
75
+ return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
76
+ }
77
+ var InMemoryStorage, STORAGE_KEYS, TrackingManager;
78
+ var init_tracking_manager = __esm({
79
+ "src/tracking/tracking-manager.ts"() {
80
+ "use strict";
81
+ InMemoryStorage = class {
82
+ constructor() {
83
+ this.map = /* @__PURE__ */ new Map();
84
+ }
85
+ getItem(key) {
86
+ return this.map.has(key) ? this.map.get(key) : null;
87
+ }
88
+ setItem(key, value) {
89
+ this.map.set(key, value);
90
+ }
91
+ removeItem(key) {
92
+ this.map.delete(key);
93
+ }
94
+ };
95
+ STORAGE_KEYS = {
96
+ userId: "deepdots.user_id",
97
+ firstSeen: "deepdots.user.first_seen",
98
+ contactAttributes: "deepdots.contact.attributes"
99
+ };
100
+ TrackingManager = class {
101
+ constructor(options) {
102
+ /** session_id cacheado, provisto por el backend (no se genera ni se persiste). */
103
+ this.sessionId = null;
104
+ this.userWasJustCreated = false;
105
+ this.storage = options.storage;
106
+ this.clientUserId = options.clientUserId;
107
+ this.now = options.now ?? (() => Date.now());
108
+ this.uuid = options.uuid ?? defaultUuid;
109
+ this.enabled = options.enabled ?? true;
110
+ }
111
+ isTrackingEnabled() {
112
+ return this.enabled;
113
+ }
114
+ setTrackingEnabled(enabled) {
115
+ this.enabled = enabled;
116
+ }
117
+ /** Resuelve el user_id según la regla: cliente > persistido > generar+persistir. */
118
+ getUserId() {
119
+ if (!this.enabled) return null;
120
+ if (this.clientUserId) return this.clientUserId;
121
+ const persisted = this.storage.getItem(STORAGE_KEYS.userId);
122
+ if (persisted) return persisted;
123
+ const id = this.uuid();
124
+ this.storage.setItem(STORAGE_KEYS.userId, id);
125
+ this.storage.setItem(STORAGE_KEYS.firstSeen, String(this.now()));
126
+ this.userWasJustCreated = true;
127
+ return id;
128
+ }
129
+ /** `true` solo en la primera sesión tras crear el user_id propio del SDK. */
130
+ isNewUser() {
131
+ return this.userWasJustCreated;
132
+ }
133
+ /** session_id actual provisto por el backend, o null si aún no llegó. */
134
+ getSessionId() {
135
+ if (!this.enabled) return null;
136
+ return this.sessionId;
137
+ }
138
+ /** Cachea el session_id devuelto por el backend (respuesta de POST /sdk/popups). */
139
+ setSessionId(sessionId) {
140
+ if (!this.enabled) return;
141
+ this.sessionId = sessionId;
142
+ }
143
+ };
144
+ }
145
+ });
146
+
33
147
  // node_modules/@magicfeedback/native/dist/magicfeedback-sdk.browser.js
34
148
  var require_magicfeedback_sdk_browser = __commonJS({
35
149
  "node_modules/@magicfeedback/native/dist/magicfeedback-sdk.browser.js"(exports, module2) {
@@ -1955,26 +2069,11 @@ var require_magicfeedback_sdk_browser = __commonJS({
1955
2069
  }
1956
2070
  });
1957
2071
 
1958
- // src/index.ts
1959
- var index_exports = {};
1960
- __export(index_exports, {
1961
- DeepdotsPopups: () => DeepdotsPopups,
1962
- POPUP_TRIGGER_CONDITION_STATUSES: () => POPUP_TRIGGER_CONDITION_STATUSES
1963
- });
1964
- module.exports = __toCommonJS(index_exports);
1965
-
1966
- // src/types/index.ts
1967
- var POPUP_TRIGGER_CONDITION_STATUSES = [
1968
- "SHOWED",
1969
- "PARTIAL",
1970
- "COMPLETED"
1971
- ];
1972
-
1973
- // src/ui/renderPopup.ts
1974
- var import_native = __toESM(require_magicfeedback_sdk_browser());
1975
-
1976
2072
  // src/assets/style.css
1977
- var style_default = `.magicfeedback-container {
2073
+ var style_default;
2074
+ var init_style = __esm({
2075
+ "src/assets/style.css"() {
2076
+ style_default = `.magicfeedback-container {
1978
2077
  width: 100%;
1979
2078
  }
1980
2079
 
@@ -2828,8 +2927,14 @@ var style_default = `.magicfeedback-container {
2828
2927
  .magicfeedback-error{
2829
2928
  margin-top: 5px;
2830
2929
  }`;
2930
+ }
2931
+ });
2831
2932
 
2832
2933
  // src/ui/renderPopup.ts
2934
+ var renderPopup_exports = {};
2935
+ __export(renderPopup_exports, {
2936
+ renderPopup: () => renderPopup
2937
+ });
2833
2938
  function ensureMagicFeedbackStyles(_popup) {
2834
2939
  const STYLE_ID = "magicfeedback-sdk-styles";
2835
2940
  if (document.getElementById(STYLE_ID)) return;
@@ -2887,7 +2992,7 @@ function ensureResponsiveStyles(_popup) {
2887
2992
  `;
2888
2993
  document.head.appendChild(style);
2889
2994
  }
2890
- async function renderPopup(container, surveyId, productId, actions, emit, onClose, env = "production", userId, style) {
2995
+ async function renderPopup(container, surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService, analyticsFeedbackSessionId) {
2891
2996
  let surveyCompletedEmitted = false;
2892
2997
  let stylesInjected = false;
2893
2998
  const isDark = style?.theme === "dark";
@@ -3198,8 +3303,8 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
3198
3303
  env: env === "production" ? "prod" : "dev"
3199
3304
  }
3200
3305
  );
3201
- const profile = userId ? [{ key: "external-user-id", value: [userId] }] : [];
3202
- formInstance = import_native.default.form(surveyId, productId, profile, profile);
3306
+ const { profile, metadata } = buildSurveyIdentity(userId ?? null, sessionId ?? null, miniService ?? null, analyticsFeedbackSessionId ?? null);
3307
+ formInstance = import_native.default.form(surveyId, productId, profile, metadata);
3203
3308
  const generateOptions = {
3204
3309
  addButton: false,
3205
3310
  getMetaData: true
@@ -3338,6 +3443,42 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
3338
3443
  setLoading(false);
3339
3444
  }
3340
3445
  }
3446
+ var import_native;
3447
+ var init_renderPopup = __esm({
3448
+ "src/ui/renderPopup.ts"() {
3449
+ "use strict";
3450
+ init_tracking_manager();
3451
+ import_native = __toESM(require_magicfeedback_sdk_browser());
3452
+ init_style();
3453
+ }
3454
+ });
3455
+
3456
+ // src/index.ts
3457
+ var index_exports = {};
3458
+ __export(index_exports, {
3459
+ BrowserPopupRenderer: () => BrowserPopupRenderer,
3460
+ ContactManager: () => ContactManager,
3461
+ DeepdotsPopups: () => DeepdotsPopups,
3462
+ InMemoryStorage: () => InMemoryStorage,
3463
+ NoopPopupRenderer: () => NoopPopupRenderer,
3464
+ POPUP_TRIGGER_CONDITION_STATUSES: () => POPUP_TRIGGER_CONDITION_STATUSES,
3465
+ ReactNativePopupRenderer: () => ReactNativePopupRenderer,
3466
+ buildAnalyticsFeedbackBody: () => buildAnalyticsFeedbackBody,
3467
+ buildSurveyHtml: () => buildSurveyHtml,
3468
+ collectRnDevice: () => collectRnDevice,
3469
+ createDefaultRenderer: () => createDefaultRenderer,
3470
+ createFeedbackSink: () => createFeedbackSink,
3471
+ mmkvStorage: () => mmkvStorage,
3472
+ setupReactNative: () => setupReactNative
3473
+ });
3474
+ module.exports = __toCommonJS(index_exports);
3475
+
3476
+ // src/types/index.ts
3477
+ var POPUP_TRIGGER_CONDITION_STATUSES = [
3478
+ "SHOWED",
3479
+ "PARTIAL",
3480
+ "COMPLETED"
3481
+ ];
3341
3482
 
3342
3483
  // src/triggers/index.ts
3343
3484
  function setupTrigger(popups, trigger) {
@@ -3516,41 +3657,125 @@ function resolveEnvironment(nodeEnv) {
3516
3657
  return ENVIRONMENT_MAP[nodeEnv || "production"];
3517
3658
  }
3518
3659
 
3660
+ // src/platform/react-native-renderer.ts
3661
+ init_tracking_manager();
3662
+
3663
+ // src/ui/surveyHtml.ts
3664
+ function buildSurveyHtml(opts) {
3665
+ const env = opts.env === "development" ? "dev" : "prod";
3666
+ const version = opts.version ?? "2.2.4";
3667
+ const cdn = `https://cdn.jsdelivr.net/npm/@magicfeedback/native@${version}/dist/magicfeedback-sdk.browser.js`;
3668
+ const sid = JSON.stringify(opts.surveyId);
3669
+ const pid = JSON.stringify(opts.productId);
3670
+ const profileJson = JSON.stringify(opts.profile ?? []);
3671
+ const metaJson = JSON.stringify(opts.metadata ?? []);
3672
+ return `<!DOCTYPE html><html><head>
3673
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
3674
+ <meta name="color-scheme" content="light"/>
3675
+ <style>html,body{margin:0;padding:0;background:transparent;font-family:-apple-system,system-ui,sans-serif}#mf{width:100%;box-sizing:border-box}#mf *{max-width:100%;box-sizing:border-box}</style>
3676
+ </head><body>
3677
+ <div id="mf"></div>
3678
+ <script>
3679
+ (function(){
3680
+ function emit(s){ try{ if(window.ReactNativeWebView){ window.ReactNativeWebView.postMessage(s); } }catch(e){} }
3681
+ function emitJSON(n,p){ try{ emit(JSON.stringify({name:n,payload:p||{}})); }catch(e){} }
3682
+ window.DeepdotsClose = function(){ emitJSON('popup_close'); };
3683
+ var initialized=false;
3684
+ function initMF(){
3685
+ if(initialized || !window.magicfeedback) return initialized;
3686
+ initialized=true;
3687
+ try{
3688
+ window.magicfeedback.init({ debug:false, env:'${env}' });
3689
+ var form = window.magicfeedback.form(${sid}, ${pid}, ${profileJson}, ${metaJson});
3690
+ window.DeepdotsForm = form;
3691
+ form.generate('mf', {
3692
+ addButton:false,
3693
+ getMetaData:true,
3694
+ onLoadedEvent:function(){ emitJSON('loaded'); },
3695
+ beforeSubmitEvent:function(){ emitJSON('before_submit'); },
3696
+ afterSubmitEvent:function(p){
3697
+ try{ if(p && p.completed){ emitJSON('survey_completed', p); } else { emitJSON('after_submit', p); } }
3698
+ catch(e){ emitJSON('after_submit'); }
3699
+ },
3700
+ onBackEvent:function(p){ emitJSON('back', p); }
3701
+ }).catch(function(e){ emit('error:init'); });
3702
+ }catch(e){ emit('error:exception'); }
3703
+ return true;
3704
+ }
3705
+ var s=document.createElement('script'); s.src=${JSON.stringify(cdn)}; s.async=true;
3706
+ s.onload=function(){ initMF(); };
3707
+ s.onerror=function(){ emit('error:load'); };
3708
+ document.head.appendChild(s);
3709
+ var tries=0, t=setInterval(function(){ if(initMF()||++tries>40){ clearInterval(t); } },150);
3710
+ })();
3711
+ <\/script>
3712
+ </body></html>`;
3713
+ }
3714
+
3519
3715
  // src/platform/react-native-renderer.ts
3520
3716
  var ReactNativePopupRenderer = class {
3521
- constructor() {
3522
- this.mounted = false;
3523
- this.lastSurveyId = null;
3717
+ constructor(options = {}) {
3718
+ this.options = options;
3524
3719
  this.emitFn = null;
3525
3720
  this.onCloseFn = null;
3526
- this.completed = false;
3721
+ this.currentSurveyId = null;
3722
+ this.partialEmitted = false;
3527
3723
  }
3528
3724
  init() {
3529
3725
  }
3530
- show(surveyId, productId, data, emit, onClose) {
3531
- this.mounted = true;
3532
- this.lastSurveyId = surveyId;
3726
+ show(surveyId, productId, _actions, emit, onClose, env = "production", userId, _style, sessionId, miniService, analyticsFeedbackSessionId) {
3533
3727
  this.emitFn = emit;
3534
3728
  this.onCloseFn = onClose;
3535
- this.completed = false;
3536
- queueMicrotask(() => emit("popup_clicked", surveyId, data));
3537
- console.log("[ReactNativePopupRenderer] (stub) show survey:", { surveyId, productId, data });
3538
- }
3539
- /** Completar la encuesta desde la capa nativa */
3540
- completeSurvey(data) {
3541
- if (!this.mounted || this.completed || !this.lastSurveyId || !this.emitFn) return;
3542
- this.completed = true;
3543
- this.emitFn("survey_completed", this.lastSurveyId, data);
3544
- this.onCloseFn?.();
3545
- this.mounted = false;
3546
- console.log("[ReactNativePopupRenderer] (stub) survey completed:", { surveyId: this.lastSurveyId, data });
3547
- this.lastSurveyId = null;
3729
+ this.currentSurveyId = surveyId;
3730
+ this.partialEmitted = false;
3731
+ const { profile, metadata } = buildSurveyIdentity(userId ?? null, sessionId ?? null, miniService ?? null, analyticsFeedbackSessionId ?? null);
3732
+ const html = buildSurveyHtml({ surveyId, productId, env, profile, metadata });
3733
+ if (this.options.onShow) {
3734
+ this.options.onShow({ surveyId, productId, html });
3735
+ } else {
3736
+ console.warn(
3737
+ "[Deepdots] ReactNativePopupRenderer sin onShow: pasa { onShow } a new ReactNativePopupRenderer({...}) para montar el WebView del survey."
3738
+ );
3739
+ }
3548
3740
  }
3549
3741
  hide() {
3550
- if (!this.mounted) return;
3551
- this.mounted = false;
3552
- console.log("[ReactNativePopupRenderer] (stub) hide survey:", this.lastSurveyId);
3553
- this.lastSurveyId = null;
3742
+ this.options.onHide?.();
3743
+ this.currentSurveyId = null;
3744
+ }
3745
+ /**
3746
+ * El host la conecta al `onMessage` del WebView. Traduce los mensajes del survey a
3747
+ * eventos del SDK: primera interacción → `popup_clicked` (PARTIAL), completado →
3748
+ * `survey_completed` (COMPLETED) y cierra el popup.
3749
+ */
3750
+ handleMessage(raw) {
3751
+ const surveyId = this.currentSurveyId;
3752
+ if (!surveyId || !this.emitFn) return;
3753
+ let name;
3754
+ try {
3755
+ name = JSON.parse(raw).name;
3756
+ } catch {
3757
+ name = raw;
3758
+ }
3759
+ switch (name) {
3760
+ case "loaded":
3761
+ case "before_submit":
3762
+ case "after_submit":
3763
+ case "back":
3764
+ if (!this.partialEmitted) {
3765
+ this.partialEmitted = true;
3766
+ this.emitFn("popup_clicked", surveyId, { action: "partial" });
3767
+ }
3768
+ break;
3769
+ case "survey_completed":
3770
+ this.emitFn("survey_completed", surveyId);
3771
+ this.onCloseFn?.();
3772
+ break;
3773
+ case "popup_close":
3774
+ this.onCloseFn?.();
3775
+ break;
3776
+ default:
3777
+ break;
3778
+ }
3554
3779
  }
3555
3780
  };
3556
3781
  function isReactNativeEnv() {
@@ -3594,12 +3819,15 @@ var BrowserPopupRenderer = class {
3594
3819
  document.body.appendChild(this.container);
3595
3820
  }
3596
3821
  }
3597
- show(surveyId, productId, actions, emit, onClose, env = "production", userId, style) {
3822
+ show(surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService, analyticsFeedbackSessionId) {
3598
3823
  if (this.visible) return;
3599
3824
  if (!this.container || !document.body.contains(this.container)) this.init();
3600
3825
  if (!this.container) return;
3601
3826
  this.visible = true;
3602
- renderPopup(this.container, surveyId, productId, actions, emit, onClose, env, userId, style);
3827
+ const container = this.container;
3828
+ void Promise.resolve().then(() => (init_renderPopup(), renderPopup_exports)).then(({ renderPopup: renderPopup2 }) => {
3829
+ renderPopup2(container, surveyId, productId, actions, emit, onClose, env, userId, style, sessionId, miniService, analyticsFeedbackSessionId);
3830
+ });
3603
3831
  }
3604
3832
  hide() {
3605
3833
  this.visible = false;
@@ -3618,9 +3846,418 @@ function createDefaultRenderer() {
3618
3846
  return new BrowserPopupRenderer();
3619
3847
  }
3620
3848
 
3849
+ // src/core/deepdots-popups.ts
3850
+ init_tracking_manager();
3851
+
3852
+ // src/analytics/analytics-manager.ts
3853
+ var dryRunSink = (payload) => {
3854
+ console.log(
3855
+ "[DeepdotsAnalytics] (dry-run \xB7 NO enviado \xB7 sin init.analytics) POST /sdk/feedback \u2192",
3856
+ JSON.stringify(payload, null, 2)
3857
+ );
3858
+ };
3859
+ var AnalyticsManager = class {
3860
+ constructor(options = {}) {
3861
+ this.events = [];
3862
+ this.attributes = {};
3863
+ this.miniService = null;
3864
+ this.miniServiceEnteredAt = 0;
3865
+ this.sink = options.sink ?? dryRunSink;
3866
+ this.now = options.now ?? (() => Date.now());
3867
+ this.publicKey = options.publicKey;
3868
+ this.language = options.language;
3869
+ this.device = options.device;
3870
+ this.platform = options.platform ?? "web";
3871
+ this.maxBatchSize = options.maxBatchSize ?? 20;
3872
+ this.onFlushNeeded = options.onFlushNeeded;
3873
+ }
3874
+ /** Actualiza campos de device en runtime (p.ej. country/city tras resolver geo). */
3875
+ updateDevice(partial) {
3876
+ this.device = { ...this.device, ...partial };
3877
+ }
3878
+ /** Mezcla user attributes (se coercionan a string). Mutable en runtime. */
3879
+ setUserAttributes(attrs) {
3880
+ for (const [k, v] of Object.entries(attrs)) {
3881
+ if (!k) continue;
3882
+ this.attributes[k] = String(v);
3883
+ }
3884
+ }
3885
+ /** Marca el inicio de un mini-service; etiqueta los eventos siguientes con `mini_service`. */
3886
+ enterMiniService(name, entryPointType) {
3887
+ this.miniService = name;
3888
+ this.miniServiceEnteredAt = this.now();
3889
+ this.track("mini_service_enter", { entry_point_type: entryPointType ?? null });
3890
+ }
3891
+ /** Cierra el mini-service activo emitiendo `mini_service_exit` con su duración (#27). No-op si no hay ninguno. */
3892
+ exitMiniService() {
3893
+ const name = this.miniService;
3894
+ if (name == null) return;
3895
+ const durationSeconds = Math.max(0, Math.round((this.now() - this.miniServiceEnteredAt) / 1e3));
3896
+ this.miniService = null;
3897
+ this.track("mini_service_exit", { mini_service: name, duration_seconds: durationSeconds });
3898
+ }
3899
+ /** Registra un evento de analítica (modelo GA: nombre + params). */
3900
+ track(name, params) {
3901
+ const merged = {
3902
+ ...this.miniService ? { mini_service: this.miniService } : {},
3903
+ ...params ?? {}
3904
+ };
3905
+ this.events.push({
3906
+ name,
3907
+ timestamp: this.now(),
3908
+ params: Object.keys(merged).length ? merged : void 0
3909
+ });
3910
+ if (this.events.length >= this.maxBatchSize) {
3911
+ this.onFlushNeeded?.();
3912
+ }
3913
+ }
3914
+ /** Mini-service activo (para inyectarlo en la metadata del survey, #33). */
3915
+ getMiniService() {
3916
+ return this.miniService;
3917
+ }
3918
+ /** Nº de eventos pendientes de flush. */
3919
+ pending() {
3920
+ return this.events.length;
3921
+ }
3922
+ /** Construye el envelope que se enviaría al endpoint de analytics. */
3923
+ buildPayload(identity) {
3924
+ return {
3925
+ publicKey: this.publicKey,
3926
+ userId: identity.userId,
3927
+ sessionId: identity.sessionId,
3928
+ context: {
3929
+ platform: this.platform,
3930
+ language: this.language,
3931
+ device: this.device,
3932
+ attributes: { ...this.attributes }
3933
+ },
3934
+ events: [...this.events]
3935
+ };
3936
+ }
3937
+ /** Envía (vía sink) el lote acumulado y vacía el buffer. No-op si no hay eventos. */
3938
+ flush(identity) {
3939
+ if (this.events.length === 0) return null;
3940
+ const payload = this.buildPayload(identity);
3941
+ this.sink(payload);
3942
+ this.events = [];
3943
+ return payload;
3944
+ }
3945
+ };
3946
+
3947
+ // src/analytics/feedback-payload.ts
3948
+ function pushKV(list, key, value) {
3949
+ if (value === void 0 || value === null || value === "") return;
3950
+ list.push({ key, value: [String(value)] });
3951
+ }
3952
+ function buildAnalyticsFeedbackBody(envelope, keys, feedbackSessionId) {
3953
+ const { context } = envelope;
3954
+ const profile = [];
3955
+ pushKV(profile, "external-user-id", envelope.userId);
3956
+ const metadata = [];
3957
+ pushKV(metadata, "deepdots_user_id", envelope.userId);
3958
+ pushKV(metadata, "deepdots_session_id", envelope.sessionId);
3959
+ pushKV(metadata, "deepdots_platform", context.platform);
3960
+ pushKV(metadata, "deepdots_language", context.language);
3961
+ if (context.device) {
3962
+ const d = context.device;
3963
+ pushKV(metadata, "deepdots_device_type", d.device_type);
3964
+ pushKV(metadata, "deepdots_os_version", d.os_version);
3965
+ pushKV(metadata, "deepdots_device_model", d.device_model);
3966
+ pushKV(metadata, "deepdots_app_version", d.app_version);
3967
+ pushKV(metadata, "deepdots_user_agent", d.user_agent);
3968
+ pushKV(metadata, "deepdots_timezone", d.timezone);
3969
+ pushKV(metadata, "deepdots_referrer", d.referrer);
3970
+ pushKV(metadata, "deepdots_viewport_size", d.viewport_size);
3971
+ pushKV(metadata, "deepdots_screen_resolution", d.screen_resolution);
3972
+ pushKV(metadata, "deepdots_pixel_ratio", d.pixel_ratio);
3973
+ pushKV(metadata, "deepdots_entry_type", d.entry_type);
3974
+ pushKV(metadata, "deepdots_page_load_ms", d.page_load_ms);
3975
+ pushKV(metadata, "deepdots_connection_type", d.connection_type);
3976
+ pushKV(metadata, "deepdots_country", d.country);
3977
+ pushKV(metadata, "deepdots_city", d.city);
3978
+ }
3979
+ for (const [k, v] of Object.entries(context.attributes ?? {})) {
3980
+ pushKV(metadata, k, v);
3981
+ }
3982
+ for (const e of envelope.events) {
3983
+ metadata.push({
3984
+ key: e.name,
3985
+ value: [JSON.stringify({ timestamp: e.timestamp, ...e.params ?? {} })]
3986
+ });
3987
+ }
3988
+ return {
3989
+ feedback: {
3990
+ text: "",
3991
+ answers: [],
3992
+ metadata,
3993
+ profile,
3994
+ finished: false
3995
+ },
3996
+ publicKey: keys.publicKey,
3997
+ integration: keys.integration,
3998
+ completed: false,
3999
+ ...feedbackSessionId ? { sessionId: feedbackSessionId } : {}
4000
+ };
4001
+ }
4002
+ function createFeedbackSink(options) {
4003
+ let feedbackSessionId;
4004
+ return (envelope) => {
4005
+ const body = buildAnalyticsFeedbackBody(envelope, options.keys, feedbackSessionId);
4006
+ const f = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
4007
+ if (!f) {
4008
+ options.log?.("[DeepdotsAnalytics] no fetch disponible; payload no enviado", body);
4009
+ return;
4010
+ }
4011
+ options.log?.("[DeepdotsAnalytics] POST /sdk/feedback \u2192", body);
4012
+ f(`${options.baseUrl}/sdk/feedback`, {
4013
+ method: "POST",
4014
+ headers: { "Content-Type": "application/json" },
4015
+ body: JSON.stringify(body)
4016
+ }).then(async (res) => {
4017
+ if (res.ok) {
4018
+ try {
4019
+ const data = await res.json();
4020
+ if (data?.sessionId && data.sessionId !== feedbackSessionId) {
4021
+ feedbackSessionId = data.sessionId;
4022
+ options.log?.("[DeepdotsAnalytics] feedbackSessionId cacheado:", feedbackSessionId);
4023
+ options.onSessionId?.(feedbackSessionId);
4024
+ }
4025
+ } catch {
4026
+ }
4027
+ }
4028
+ }).catch((err) => options.log?.("[DeepdotsAnalytics] error enviando feedback", err));
4029
+ };
4030
+ }
4031
+
4032
+ // src/tracking/navigation-observer.ts
4033
+ var UUID_PREFIX = /^[0-9a-f]{8}-[0-9a-f]{4}-/i;
4034
+ function normalizeScreen(href) {
4035
+ let pathAndHash;
4036
+ try {
4037
+ const u = new URL(href, "http://deepdots.local");
4038
+ pathAndHash = u.pathname + (u.hash || "");
4039
+ } catch {
4040
+ pathAndHash = href;
4041
+ }
4042
+ pathAndHash = pathAndHash.replace(/\?[^#]*/g, "");
4043
+ const collapsed = pathAndHash.split("/").map((seg) => /^[0-9]+$/.test(seg) || UUID_PREFIX.test(seg) ? ":id" : seg).join("/");
4044
+ return collapsed || "/";
4045
+ }
4046
+ var NavigationObserver = class {
4047
+ constructor(options = {}) {
4048
+ this.listeners = /* @__PURE__ */ new Set();
4049
+ this.currentScreen = null;
4050
+ this.entryAt = 0;
4051
+ this.installed = false;
4052
+ this.domHandlers = [];
4053
+ this.now = options.now ?? (() => Date.now());
4054
+ }
4055
+ onVisit(listener) {
4056
+ this.listeners.add(listener);
4057
+ }
4058
+ /** Fija la pantalla inicial (sin emitir visita; su duración se cierra al salir). */
4059
+ begin(href) {
4060
+ this.currentScreen = normalizeScreen(href);
4061
+ this.entryAt = this.now();
4062
+ }
4063
+ /** Navega a una pantalla nueva: cierra la anterior (emite visita) y abre la nueva. */
4064
+ visit(href) {
4065
+ const next = normalizeScreen(href);
4066
+ if (next === this.currentScreen) return;
4067
+ this.closeCurrent();
4068
+ this.currentScreen = next;
4069
+ this.entryAt = this.now();
4070
+ }
4071
+ /** Cierra la pantalla actual emitiendo su visita (p. ej. al cerrar la app/pestaña). */
4072
+ stop() {
4073
+ this.closeCurrent();
4074
+ this.currentScreen = null;
4075
+ }
4076
+ closeCurrent() {
4077
+ if (this.currentScreen == null) return;
4078
+ const exit = this.now();
4079
+ const visit = {
4080
+ screen: this.currentScreen,
4081
+ entry: this.entryAt,
4082
+ exit,
4083
+ durationSeconds: Math.max(0, Math.round((exit - this.entryAt) / 1e3))
4084
+ };
4085
+ this.listeners.forEach((l) => {
4086
+ try {
4087
+ l(visit);
4088
+ } catch {
4089
+ }
4090
+ });
4091
+ }
4092
+ /** Instala los hooks de History en el navegador y registra la pantalla actual. */
4093
+ install() {
4094
+ if (this.installed || typeof window === "undefined" || typeof history === "undefined") return;
4095
+ this.installed = true;
4096
+ const handle = () => this.visit(window.location.href);
4097
+ const originalPush = history.pushState.bind(history);
4098
+ const originalReplace = history.replaceState.bind(history);
4099
+ history.pushState = (data, unused, url) => {
4100
+ const r = originalPush(data, unused, url);
4101
+ handle();
4102
+ return r;
4103
+ };
4104
+ history.replaceState = (data, unused, url) => {
4105
+ const r = originalReplace(data, unused, url);
4106
+ handle();
4107
+ return r;
4108
+ };
4109
+ window.addEventListener("popstate", handle);
4110
+ window.addEventListener("hashchange", handle);
4111
+ this.domHandlers.push(() => {
4112
+ history.pushState = originalPush;
4113
+ history.replaceState = originalReplace;
4114
+ window.removeEventListener("popstate", handle);
4115
+ window.removeEventListener("hashchange", handle);
4116
+ });
4117
+ this.begin(window.location.href);
4118
+ }
4119
+ uninstall() {
4120
+ this.domHandlers.forEach((off) => off());
4121
+ this.domHandlers = [];
4122
+ this.installed = false;
4123
+ }
4124
+ };
4125
+
4126
+ // src/analytics/device-info.ts
4127
+ function parseDeviceType(ua) {
4128
+ const s = (ua || "").toLowerCase();
4129
+ if (/ipad|tablet|playbook|silk/.test(s) || /android/.test(s) && !/mobile/.test(s)) {
4130
+ return "tablet";
4131
+ }
4132
+ if (/mobi|iphone|ipod|windows phone/.test(s) || /android/.test(s) && /mobile/.test(s)) {
4133
+ return "mobile";
4134
+ }
4135
+ return "desktop";
4136
+ }
4137
+ function getEntryType() {
4138
+ if (typeof performance === "undefined") return void 0;
4139
+ const entries = performance.getEntriesByType("navigation");
4140
+ if (!entries.length) return void 0;
4141
+ return entries[0].type;
4142
+ }
4143
+ function getPageLoadMs() {
4144
+ if (typeof performance === "undefined") return void 0;
4145
+ const entries = performance.getEntriesByType("navigation");
4146
+ if (!entries.length) return void 0;
4147
+ const load = entries[0].loadEventEnd;
4148
+ return load > 0 ? Math.round(load) : void 0;
4149
+ }
4150
+ function getConnectionType() {
4151
+ if (typeof navigator === "undefined") return void 0;
4152
+ return navigator.connection?.effectiveType ?? void 0;
4153
+ }
4154
+ function collectDeviceInfo(appVersion) {
4155
+ const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
4156
+ return {
4157
+ device_type: parseDeviceType(ua),
4158
+ user_agent: ua || void 0,
4159
+ app_version: appVersion,
4160
+ timezone: typeof Intl !== "undefined" ? Intl.DateTimeFormat().resolvedOptions().timeZone : void 0,
4161
+ referrer: typeof document !== "undefined" && document.referrer ? document.referrer : void 0,
4162
+ viewport_size: typeof window !== "undefined" ? `${window.innerWidth}x${window.innerHeight}` : void 0,
4163
+ screen_resolution: typeof screen !== "undefined" ? `${screen.width}x${screen.height}` : void 0,
4164
+ pixel_ratio: typeof window !== "undefined" ? window.devicePixelRatio : void 0,
4165
+ entry_type: getEntryType(),
4166
+ page_load_ms: getPageLoadMs(),
4167
+ connection_type: getConnectionType()
4168
+ };
4169
+ }
4170
+
4171
+ // src/analytics/geo-info.ts
4172
+ async function collectGeoInfo(fetchImpl) {
4173
+ const f = fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
4174
+ if (!f) return null;
4175
+ try {
4176
+ const res = await f("https://ipapi.co/json/");
4177
+ if (!res.ok) return null;
4178
+ const data = await res.json();
4179
+ const country = data.country_code || void 0;
4180
+ const city = data.city || void 0;
4181
+ if (!country && !city) return null;
4182
+ return { country, city };
4183
+ } catch {
4184
+ return null;
4185
+ }
4186
+ }
4187
+
4188
+ // src/analytics/engagement-tracker.ts
4189
+ var EngagementTracker = class {
4190
+ constructor(now = () => Date.now()) {
4191
+ this.activeMs = 0;
4192
+ this.lastResumeAt = null;
4193
+ this.now = now;
4194
+ }
4195
+ /** Reanuda el conteo (pestaña visible / app en foreground). Idempotente. */
4196
+ resume() {
4197
+ if (this.lastResumeAt === null) this.lastResumeAt = this.now();
4198
+ }
4199
+ /** Pausa el conteo (pestaña oculta / app en background), acumulando el tramo activo. */
4200
+ pause() {
4201
+ if (this.lastResumeAt !== null) {
4202
+ this.activeMs += this.now() - this.lastResumeAt;
4203
+ this.lastResumeAt = null;
4204
+ }
4205
+ }
4206
+ /** Devuelve los ms activos acumulados desde la última lectura y reinicia (sin parar el timer). */
4207
+ consume() {
4208
+ if (this.lastResumeAt !== null) {
4209
+ const t = this.now();
4210
+ this.activeMs += t - this.lastResumeAt;
4211
+ this.lastResumeAt = t;
4212
+ }
4213
+ const ms = this.activeMs;
4214
+ this.activeMs = 0;
4215
+ return ms;
4216
+ }
4217
+ isActive() {
4218
+ return this.lastResumeAt !== null;
4219
+ }
4220
+ };
4221
+
4222
+ // src/contact/contact-manager.ts
4223
+ init_tracking_manager();
4224
+ function stableSerialize(attrs) {
4225
+ const sorted = {};
4226
+ for (const key of Object.keys(attrs).sort()) {
4227
+ sorted[key] = attrs[key];
4228
+ }
4229
+ return JSON.stringify(sorted);
4230
+ }
4231
+ var ContactManager = class {
4232
+ constructor(options) {
4233
+ this.storage = options.storage;
4234
+ this.publicKey = options.publicKey;
4235
+ this.userId = options.userId;
4236
+ this.post = options.post;
4237
+ }
4238
+ /**
4239
+ * Envía los atributos si cambiaron respecto a lo último enviado.
4240
+ * @returns `true` si se envió, `false` si no hubo cambios.
4241
+ */
4242
+ async setAttributes(attributes) {
4243
+ const serialized = stableSerialize(attributes);
4244
+ const last = this.storage.getItem(STORAGE_KEYS.contactAttributes);
4245
+ if (last === serialized) return false;
4246
+ await this.post({
4247
+ publicKey: this.publicKey,
4248
+ userId: this.userId,
4249
+ userAttributes: attributes
4250
+ });
4251
+ this.storage.setItem(STORAGE_KEYS.contactAttributes, serialized);
4252
+ return true;
4253
+ }
4254
+ };
4255
+
3621
4256
  // src/core/deepdots-popups.ts
3622
4257
  var EXIT_QUEUE_STORAGE_KEY = "__deepdots_exit_popup_queue__";
3623
4258
  var DAY_IN_MS = 24 * 60 * 60 * 1e3;
4259
+ var ANALYTICS_MAX_BATCH_SIZE = 20;
4260
+ var ANALYTICS_FLUSH_INTERVAL_MS = 3e4;
3624
4261
  var DeepdotsPopups = class {
3625
4262
  constructor() {
3626
4263
  this.config = null;
@@ -3642,6 +4279,22 @@ var DeepdotsPopups = class {
3642
4279
  this.deferredExitTimers = [];
3643
4280
  this.baseUrl = "";
3644
4281
  this.env = "production";
4282
+ /** Identidad + sesión (Fase 1 tracking). Null hasta init(). */
4283
+ this.tracking = null;
4284
+ /** Capa de analytics (canal separado del feedback). Null hasta init(). */
4285
+ this.analytics = null;
4286
+ /** feedbackSessionId cacheado del canal de analytics (devuelto por POST /sdk/feedback). */
4287
+ this.analyticsFeedbackSessionId = void 0;
4288
+ /** Timer del flush periódico de analytics (cada ANALYTICS_FLUSH_INTERVAL_MS). */
4289
+ this.analyticsFlushTimer = void 0;
4290
+ /** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
4291
+ this.navObserver = null;
4292
+ /** Tiempo activo (engagement time, #8). */
4293
+ this.engagement = null;
4294
+ /** Atributos internos del usuario identificado → POST /sdk/popups/contact. Null si no hay userId. */
4295
+ this.contact = null;
4296
+ /** Marca si ya se inició la navegación manual (setScreen) — para RN sin History API. */
4297
+ this.navStarted = false;
3645
4298
  }
3646
4299
  /** Initialize the SDK with configuration */
3647
4300
  init(config) {
@@ -3654,33 +4307,207 @@ var DeepdotsPopups = class {
3654
4307
  this.env = config.nodeEnv || "production";
3655
4308
  this.config = {
3656
4309
  apiKey: config.apiKey || void 0,
3657
- mode: config.mode || "client",
3658
4310
  debug: config.debug || false,
3659
- popups: config.popups || [],
3660
4311
  userId: config.userId || void 0
3661
4312
  };
4313
+ const storage = config.storage ?? createDefaultStorage();
4314
+ this.tracking = new TrackingManager({
4315
+ storage,
4316
+ clientUserId: this.config.userId,
4317
+ enabled: config.trackingEnabled ?? true
4318
+ });
4319
+ if (this.config.userId) {
4320
+ const userId = this.config.userId;
4321
+ const publicKey = this.config.apiKey ?? "";
4322
+ this.contact = new ContactManager({
4323
+ storage,
4324
+ publicKey,
4325
+ userId,
4326
+ post: (body) => this.postContact(body)
4327
+ });
4328
+ if (config.contactAttributes) {
4329
+ void this.setContactAttributes(config.contactAttributes);
4330
+ }
4331
+ }
4332
+ this.log("tracking \xB7 user_id:", this.tracking.getUserId(), "\xB7 new_user:", this.tracking.isNewUser(), "\xB7 enabled:", this.tracking.isTrackingEnabled());
4333
+ const analyticsSink = config.analytics ? createFeedbackSink({
4334
+ baseUrl: this.baseUrl,
4335
+ keys: config.analytics,
4336
+ log: (...a) => this.log(...a),
4337
+ onSessionId: (id) => {
4338
+ this.analyticsFeedbackSessionId = id;
4339
+ }
4340
+ }) : void 0;
4341
+ this.analytics = new AnalyticsManager({
4342
+ sink: analyticsSink,
4343
+ publicKey: config.analytics?.publicKey ?? this.config.apiKey,
4344
+ language: typeof navigator !== "undefined" ? navigator.language : void 0,
4345
+ platform: config.platform ?? "web",
4346
+ device: config.device ?? collectDeviceInfo(config.appVersion),
4347
+ maxBatchSize: ANALYTICS_MAX_BATCH_SIZE,
4348
+ onFlushNeeded: () => this.flushAnalytics()
4349
+ });
4350
+ collectGeoInfo().then((geo) => {
4351
+ if (geo) this.analytics?.updateDevice(geo);
4352
+ }).catch(() => {
4353
+ });
4354
+ this.navObserver = new NavigationObserver();
4355
+ this.navObserver.onVisit((v) => this.track("page_view", { screen: v.screen, duration_seconds: v.durationSeconds }));
4356
+ this.navObserver.install();
4357
+ this.engagement = new EngagementTracker();
4358
+ this.engagement.resume();
4359
+ this.setupAnalyticsFlush();
3662
4360
  this.initialized = true;
3663
4361
  this.log("SDK initialized", this.config);
3664
4362
  if (this.renderer.init) this.renderer.init();
3665
4363
  this.setupPopupContainer();
3666
- if (this.config.mode === "client") {
3667
- this.popupDefinitions = this.validatePopupDefinitions(this.config.popups || []);
4364
+ this.fetchPopupsFromServer().then((defs) => {
4365
+ this.popupDefinitions = this.validatePopupDefinitions(defs);
3668
4366
  this.popupsLoaded = true;
4367
+ this.log("Popups loaded from API", this.popupDefinitions);
3669
4368
  this.configureTriggersFromDefinitions();
3670
4369
  this.processDeferredExitQueue();
4370
+ if (this.pendingAutoLaunch) {
4371
+ this.startTriggers();
4372
+ }
4373
+ });
4374
+ }
4375
+ /** User id actual (generado por el SDK o provisto por el cliente). Null si tracking off. */
4376
+ getUserId() {
4377
+ return this.tracking?.getUserId() ?? null;
4378
+ }
4379
+ /** Session id de navegación actual. Null si tracking off. */
4380
+ getSessionId() {
4381
+ return this.tracking?.getSessionId() ?? null;
4382
+ }
4383
+ /** Activa/desactiva el tracking (identidad + sesión + analytics). Kill-switch del contrato §7bis. */
4384
+ setTrackingEnabled(enabled) {
4385
+ this.tracking?.setTrackingEnabled(enabled);
4386
+ this.log("tracking \xB7 setTrackingEnabled:", enabled, "\xB7 session_id:", this.tracking?.getSessionId() ?? null);
4387
+ }
4388
+ // ───────── Analytics (canal separado del feedback, vinculado por user_id) ─────────
4389
+ /** Registra un evento de analítica (modelo GA: nombre + params). No-op si tracking off. */
4390
+ track(name, params) {
4391
+ if (!this.tracking?.isTrackingEnabled()) return;
4392
+ this.analytics?.track(name, params);
4393
+ this.log("analytics \xB7 track:", name, params ?? {});
4394
+ }
4395
+ /** User attributes para breakdowns (registration_status, pass_type, sector, pass_status…). */
4396
+ setUserAttributes(attributes) {
4397
+ if (!this.tracking?.isTrackingEnabled()) return;
4398
+ this.analytics?.setUserAttributes(attributes);
4399
+ }
4400
+ /**
4401
+ * Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
4402
+ * Contact del backend para segmentación/targeting de popups. Identificada por el `userId` del
4403
+ * init. Solo envía si los atributos cambiaron (diff en storage). No-op si tracking off o sin userId.
4404
+ * @returns `true` si se envió al backend, `false` si no hubo cambios o está deshabilitado.
4405
+ */
4406
+ async setContactAttributes(attributes) {
4407
+ if (!this.tracking?.isTrackingEnabled()) return false;
4408
+ if (!this.contact) return false;
4409
+ return this.contact.setAttributes(attributes);
4410
+ }
4411
+ /** Marca el inicio de un mini-service; etiqueta los eventos siguientes. No-op si tracking off. */
4412
+ enterMiniService(name, entryPointType) {
4413
+ if (!this.tracking?.isTrackingEnabled()) return;
4414
+ this.analytics?.enterMiniService(name, entryPointType);
4415
+ this.log("analytics \xB7 enterMiniService:", name, entryPointType ?? "");
4416
+ }
4417
+ /** Cierra el mini-service activo (emite `mini_service_exit` con duración, #27). No-op si tracking off. */
4418
+ exitMiniService() {
4419
+ if (!this.tracking?.isTrackingEnabled()) return;
4420
+ this.analytics?.exitMiniService();
4421
+ this.log("analytics \xB7 exitMiniService");
4422
+ }
4423
+ /** Findability (#31/#35): registra una búsqueda. `has_results` se deriva de `resultsCount`. */
4424
+ trackSearch(query, resultsCount, params) {
4425
+ this.track("search", { query, results_count: resultsCount, has_results: resultsCount > 0, ...params ?? {} });
4426
+ }
4427
+ /** Findability friction (#34/#35): señal de fricción con su `friction_topic`. */
4428
+ trackFindabilityFriction(frictionTopic, params) {
4429
+ this.track("findability_friction", { friction_topic: frictionTopic, ...params ?? {} });
4430
+ }
4431
+ /** Funnel: un paso del embudo, correlacionado por `taskId`. El backend reconstruye conversión/drop-off/tiempo. */
4432
+ trackFunnelStep(funnel, step, taskId, params) {
4433
+ this.track("funnel_step", { funnel, step, task_id: taskId, ...params ?? {} });
4434
+ }
4435
+ /**
4436
+ * Navegación MANUAL (entornos sin History API, p. ej. React Native con React Navigation).
4437
+ * El host la llama en cada cambio de pantalla; emite `page_view` al salir de la anterior.
4438
+ */
4439
+ setScreen(name) {
4440
+ if (!this.navObserver) return;
4441
+ if (!this.navStarted) {
4442
+ this.navObserver.begin(name);
4443
+ this.navStarted = true;
3671
4444
  } else {
3672
- this.fetchPopupsFromServer().then((defs) => {
3673
- this.popupDefinitions = this.validatePopupDefinitions(defs);
3674
- this.popupsLoaded = true;
3675
- this.log("Popups loaded from server (fake)", this.popupDefinitions);
3676
- this.configureTriggersFromDefinitions();
3677
- this.processDeferredExitQueue();
3678
- if (this.pendingAutoLaunch) {
3679
- this.startTriggers();
3680
- }
3681
- });
4445
+ this.navObserver.visit(name);
3682
4446
  }
3683
4447
  }
4448
+ /** App a foreground (RN: AppState 'active'): reanuda el engagement time. */
4449
+ onForeground() {
4450
+ this.engagement?.resume();
4451
+ }
4452
+ /**
4453
+ * App a background (RN: AppState 'background'): cierra pantalla actual (page_view),
4454
+ * mini-service (mini_service_exit), emite engagement y hace flush del lote.
4455
+ */
4456
+ onBackground() {
4457
+ this.navObserver?.stop();
4458
+ this.navStarted = false;
4459
+ this.exitMiniService();
4460
+ this.flushEngagement();
4461
+ this.engagement?.pause();
4462
+ this.flushAnalytics();
4463
+ }
4464
+ /** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
4465
+ previewAnalytics() {
4466
+ return this.analytics?.buildPayload(this.analyticsIdentity()) ?? {
4467
+ userId: null,
4468
+ sessionId: null,
4469
+ context: { platform: "web", attributes: {} },
4470
+ events: []
4471
+ };
4472
+ }
4473
+ /** Envía (hoy dry-run → console.log) el lote acumulado de analytics y vacía el buffer. */
4474
+ flushAnalytics() {
4475
+ if (!this.tracking?.isTrackingEnabled()) return;
4476
+ this.analytics?.flush(this.analyticsIdentity());
4477
+ }
4478
+ /** Emite un evento `user_engagement` con el tiempo activo acumulado (#8). */
4479
+ flushEngagement() {
4480
+ if (!this.tracking?.isTrackingEnabled()) return;
4481
+ const ms = this.engagement?.consume() ?? 0;
4482
+ if (ms > 0) this.track("user_engagement", { engagement_time_msec: ms });
4483
+ }
4484
+ analyticsIdentity() {
4485
+ return {
4486
+ userId: this.tracking?.getUserId() ?? null,
4487
+ sessionId: this.tracking?.getSessionId() ?? null
4488
+ };
4489
+ }
4490
+ /** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
4491
+ setupAnalyticsFlush() {
4492
+ if (typeof document === "undefined" || typeof window === "undefined") return;
4493
+ this.analyticsFlushTimer = setInterval(() => this.flushAnalytics(), ANALYTICS_FLUSH_INTERVAL_MS);
4494
+ window.addEventListener("pagehide", () => {
4495
+ clearInterval(this.analyticsFlushTimer);
4496
+ this.navObserver?.stop();
4497
+ this.exitMiniService();
4498
+ this.flushEngagement();
4499
+ this.flushAnalytics();
4500
+ });
4501
+ document.addEventListener("visibilitychange", () => {
4502
+ if (document.visibilityState === "hidden") {
4503
+ this.flushEngagement();
4504
+ this.engagement?.pause();
4505
+ this.flushAnalytics();
4506
+ } else if (document.visibilityState === "visible") {
4507
+ this.engagement?.resume();
4508
+ }
4509
+ });
4510
+ }
3684
4511
  /** Enable auto-launch functionality with configured triggers */
3685
4512
  autoLaunch() {
3686
4513
  if (!this.initialized) {
@@ -3698,35 +4525,6 @@ var DeepdotsPopups = class {
3698
4525
  this.log("Auto-launch enabled");
3699
4526
  this.triggers.forEach((trigger) => setupTrigger(this, trigger));
3700
4527
  }
3701
- /** Show a popup immediately (supports legacy ShowOptions and new PopupDefinition) */
3702
- show(options) {
3703
- if (!this.initialized) {
3704
- throw new Error("SDK not initialized. Call init() first.");
3705
- }
3706
- const isDefinition = options.id !== void 0;
3707
- if (isDefinition) {
3708
- const def = options;
3709
- this.log("Showing popup (definition)", def);
3710
- this.surveyToPopupId.set(def.surveyId, def.id);
3711
- this.lastShown.set(def.id, Date.now());
3712
- this.renderPopup(def.surveyId, def.productId, def.actions, def.style);
3713
- this.emitEvent("popup_shown", def.surveyId, { popupId: def.id });
3714
- } else {
3715
- const opt = options;
3716
- this.log("Showing popup (legacy options)", opt);
3717
- this.renderPopup(opt.surveyId, opt.productId, void 0);
3718
- this.emitEvent("popup_shown", opt.surveyId);
3719
- }
3720
- }
3721
- /** Mostrar popup por id de definición (usa surveyId interno) */
3722
- showByPopupId(popupId) {
3723
- const def = this.popupDefinitions.find((popup) => popup.id === popupId);
3724
- if (!def) {
3725
- this.log("Popup definition not found", popupId);
3726
- return;
3727
- }
3728
- this.show(def);
3729
- }
3730
4528
  /** Configure triggers for auto-launching popups (manual) */
3731
4529
  configureTriggers(triggers) {
3732
4530
  if (!this.initialized) {
@@ -3795,7 +4593,11 @@ var DeepdotsPopups = class {
3795
4593
  this.showDefinition(matched);
3796
4594
  }
3797
4595
  showDefinition(def) {
3798
- this.show(def);
4596
+ this.log("Showing popup (definition)", def);
4597
+ this.surveyToPopupId.set(def.surveyId, def.id);
4598
+ this.lastShown.set(def.id, Date.now());
4599
+ this.renderPopup(def.surveyId, def.productId, def.actions, def.style);
4600
+ this.emitEvent("popup_shown", def.surveyId, { popupId: def.id });
3799
4601
  }
3800
4602
  shouldShow(def, pathUrl, skipPathCheck = false) {
3801
4603
  if (!skipPathCheck && !this.matchesSegmentsPath(def, pathUrl)) {
@@ -4039,6 +4841,26 @@ var DeepdotsPopups = class {
4039
4841
  return [];
4040
4842
  }
4041
4843
  }
4844
+ /** Envía los atributos de contact a la API (`POST /sdk/popups/contact`). */
4845
+ async postContact(body) {
4846
+ const baseUrl = this.baseUrl;
4847
+ if (!baseUrl) return;
4848
+ const endpoint = `${baseUrl}/sdk/popups/contact`;
4849
+ try {
4850
+ const response = await fetch(endpoint, {
4851
+ method: "POST",
4852
+ headers: { "Content-Type": "application/json" },
4853
+ body: JSON.stringify(body)
4854
+ });
4855
+ if (!response.ok) {
4856
+ this.log("Failed to post contact", response.status, response.statusText);
4857
+ return;
4858
+ }
4859
+ this.log("contact \xB7 sent attributes for user:", body.userId);
4860
+ } catch (error) {
4861
+ this.log("Error posting contact", error);
4862
+ }
4863
+ }
4042
4864
  /** Notifica eventos de popup a la API */
4043
4865
  async postPopupEvent(status, popupId, userId) {
4044
4866
  const apiKey = this.config?.apiKey;
@@ -4060,6 +4882,15 @@ var DeepdotsPopups = class {
4060
4882
  });
4061
4883
  if (!response.ok) {
4062
4884
  this.log("Failed to post popup event", response.status, response.statusText);
4885
+ return;
4886
+ }
4887
+ try {
4888
+ const data = await response.json();
4889
+ if (data && typeof data.sessionId === "string") {
4890
+ this.tracking?.setSessionId(data.sessionId);
4891
+ this.log("tracking \xB7 session_id from backend:", data.sessionId);
4892
+ }
4893
+ } catch {
4063
4894
  }
4064
4895
  } catch (error) {
4065
4896
  this.log("Error posting popup event", error);
@@ -4102,7 +4933,11 @@ var DeepdotsPopups = class {
4102
4933
  }
4103
4934
  /** Render the popup UI */
4104
4935
  renderPopup(surveyId, productId, actions, style) {
4105
- const userId = this.config?.userId;
4936
+ const userId = this.tracking?.getUserId() ?? this.config?.userId;
4937
+ const sessionId = this.tracking?.getSessionId() ?? void 0;
4938
+ const miniService = this.analytics?.getMiniService() ?? void 0;
4939
+ const analyticsFeedbackSessionId = this.analyticsFeedbackSessionId;
4940
+ this.log("tracking \xB7 survey identity \u2192", { userId: userId ?? null, sessionId: sessionId ?? null, miniService: miniService ?? null, analyticsFeedbackSessionId: analyticsFeedbackSessionId ?? null });
4106
4941
  if (this.renderer) {
4107
4942
  this.renderer.show(
4108
4943
  surveyId,
@@ -4112,22 +4947,31 @@ var DeepdotsPopups = class {
4112
4947
  () => this.hidePopup(),
4113
4948
  this.env,
4114
4949
  userId,
4115
- style
4950
+ style,
4951
+ sessionId,
4952
+ miniService,
4953
+ analyticsFeedbackSessionId
4116
4954
  );
4117
4955
  return;
4118
4956
  }
4119
4957
  if (!this.popupContainer) return;
4120
- renderPopup(
4121
- this.popupContainer,
4122
- surveyId,
4123
- productId,
4124
- actions,
4125
- (type, id, payload) => this.emitEvent(type, id, payload),
4126
- () => this.hidePopup(),
4127
- this.env,
4128
- userId,
4129
- style
4130
- );
4958
+ const container = this.popupContainer;
4959
+ void Promise.resolve().then(() => (init_renderPopup(), renderPopup_exports)).then(({ renderPopup: renderPopup2 }) => {
4960
+ renderPopup2(
4961
+ container,
4962
+ surveyId,
4963
+ productId,
4964
+ actions,
4965
+ (type, id, payload) => this.emitEvent(type, id, payload),
4966
+ () => this.hidePopup(),
4967
+ this.env,
4968
+ userId,
4969
+ style,
4970
+ sessionId,
4971
+ miniService,
4972
+ analyticsFeedbackSessionId
4973
+ );
4974
+ });
4131
4975
  }
4132
4976
  /** Hide the popup */
4133
4977
  hidePopup() {
@@ -4157,7 +5001,7 @@ var DeepdotsPopups = class {
4157
5001
  if (popupId) {
4158
5002
  const userIdFromData = data?.userId;
4159
5003
  const status = type === "popup_shown" ? "SHOWED" /* SHOWED */ : type === "survey_completed" ? "COMPLETED" /* COMPLETED */ : "PARTIAL" /* PARTIAL */;
4160
- void this.postPopupEvent(status, popupId, userIdFromData || this.config?.userId);
5004
+ void this.postPopupEvent(status, popupId, userIdFromData || this.tracking?.getUserId() || this.config?.userId);
4161
5005
  } else {
4162
5006
  this.debug("No popupId available to post event", { type, surveyId });
4163
5007
  }
@@ -4293,4 +5137,41 @@ var DeepdotsPopups = class {
4293
5137
  return defs.map((def) => this.normalizePopupDefinition(def)).filter((def) => !!def && this.isPopupDefinition(def));
4294
5138
  }
4295
5139
  };
5140
+
5141
+ // src/index.ts
5142
+ init_tracking_manager();
5143
+
5144
+ // src/react-native/setup.ts
5145
+ function mmkvStorage(mmkv) {
5146
+ return {
5147
+ getItem: (k) => mmkv.getString(k) ?? null,
5148
+ setItem: (k, v) => mmkv.set(k, v),
5149
+ removeItem: (k) => mmkv.delete(k)
5150
+ };
5151
+ }
5152
+ function collectRnDevice(d) {
5153
+ return {
5154
+ device_type: d.isTablet?.() ? "tablet" : "mobile",
5155
+ os_version: d.getSystemVersion?.(),
5156
+ device_model: d.getDeviceId?.(),
5157
+ app_version: d.getVersion?.()
5158
+ };
5159
+ }
5160
+ function setupReactNative(sdk, config, deps = {}) {
5161
+ if (deps.renderer) sdk.setRenderer(deps.renderer);
5162
+ const storage = deps.mmkv ? mmkvStorage(deps.mmkv) : config.storage;
5163
+ const device = deps.deviceInfo ? collectRnDevice(deps.deviceInfo) : config.device;
5164
+ const platform = deps.platform ?? config.platform;
5165
+ sdk.init({ ...config, storage, device, platform });
5166
+ let subscription;
5167
+ if (deps.appState?.addEventListener) {
5168
+ subscription = deps.appState.addEventListener("change", (state) => {
5169
+ if (state === "active") sdk.onForeground();
5170
+ else sdk.onBackground();
5171
+ });
5172
+ }
5173
+ return () => {
5174
+ if (subscription && typeof subscription.remove === "function") subscription.remove();
5175
+ };
5176
+ }
4296
5177
  //# sourceMappingURL=index.js.map