@magicfeedback/popup-sdk 1.0.15 → 1.1.1

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,116 @@ 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) {
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
+ return { profile, metadata };
61
+ }
62
+ function defaultUuid() {
63
+ const g = typeof globalThis !== "undefined" ? globalThis : {};
64
+ if (g.crypto?.randomUUID) return g.crypto.randomUUID();
65
+ const bytes = new Uint8Array(16);
66
+ if (g.crypto?.getRandomValues) {
67
+ g.crypto.getRandomValues(bytes);
68
+ } else {
69
+ for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
70
+ }
71
+ bytes[6] = bytes[6] & 15 | 64;
72
+ bytes[8] = bytes[8] & 63 | 128;
73
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0"));
74
+ 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("")}`;
75
+ }
76
+ var InMemoryStorage, STORAGE_KEYS, TrackingManager;
77
+ var init_tracking_manager = __esm({
78
+ "src/tracking/tracking-manager.ts"() {
79
+ "use strict";
80
+ InMemoryStorage = class {
81
+ constructor() {
82
+ this.map = /* @__PURE__ */ new Map();
83
+ }
84
+ getItem(key) {
85
+ return this.map.has(key) ? this.map.get(key) : null;
86
+ }
87
+ setItem(key, value) {
88
+ this.map.set(key, value);
89
+ }
90
+ removeItem(key) {
91
+ this.map.delete(key);
92
+ }
93
+ };
94
+ STORAGE_KEYS = {
95
+ userId: "deepdots.user_id",
96
+ firstSeen: "deepdots.user.first_seen",
97
+ contactAttributes: "deepdots.contact.attributes"
98
+ };
99
+ TrackingManager = class {
100
+ constructor(options) {
101
+ /** session_id cacheado, provisto por el backend (no se genera ni se persiste). */
102
+ this.sessionId = null;
103
+ this.userWasJustCreated = false;
104
+ this.storage = options.storage;
105
+ this.clientUserId = options.clientUserId;
106
+ this.now = options.now ?? (() => Date.now());
107
+ this.uuid = options.uuid ?? defaultUuid;
108
+ this.enabled = options.enabled ?? true;
109
+ }
110
+ isTrackingEnabled() {
111
+ return this.enabled;
112
+ }
113
+ setTrackingEnabled(enabled) {
114
+ this.enabled = enabled;
115
+ }
116
+ /** Resuelve el user_id según la regla: cliente > persistido > generar+persistir. */
117
+ getUserId() {
118
+ if (!this.enabled) return null;
119
+ if (this.clientUserId) return this.clientUserId;
120
+ const persisted = this.storage.getItem(STORAGE_KEYS.userId);
121
+ if (persisted) return persisted;
122
+ const id = this.uuid();
123
+ this.storage.setItem(STORAGE_KEYS.userId, id);
124
+ this.storage.setItem(STORAGE_KEYS.firstSeen, String(this.now()));
125
+ this.userWasJustCreated = true;
126
+ return id;
127
+ }
128
+ /** `true` solo en la primera sesión tras crear el user_id propio del SDK. */
129
+ isNewUser() {
130
+ return this.userWasJustCreated;
131
+ }
132
+ /** session_id actual provisto por el backend, o null si aún no llegó. */
133
+ getSessionId() {
134
+ if (!this.enabled) return null;
135
+ return this.sessionId;
136
+ }
137
+ /** Cachea el session_id devuelto por el backend (respuesta de POST /sdk/popups). */
138
+ setSessionId(sessionId) {
139
+ if (!this.enabled) return;
140
+ this.sessionId = sessionId;
141
+ }
142
+ };
143
+ }
144
+ });
145
+
33
146
  // node_modules/@magicfeedback/native/dist/magicfeedback-sdk.browser.js
34
147
  var require_magicfeedback_sdk_browser = __commonJS({
35
148
  "node_modules/@magicfeedback/native/dist/magicfeedback-sdk.browser.js"(exports, module2) {
@@ -1955,26 +2068,11 @@ var require_magicfeedback_sdk_browser = __commonJS({
1955
2068
  }
1956
2069
  });
1957
2070
 
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
2071
  // src/assets/style.css
1977
- var style_default = `.magicfeedback-container {
2072
+ var style_default;
2073
+ var init_style = __esm({
2074
+ "src/assets/style.css"() {
2075
+ style_default = `.magicfeedback-container {
1978
2076
  width: 100%;
1979
2077
  }
1980
2078
 
@@ -2828,8 +2926,14 @@ var style_default = `.magicfeedback-container {
2828
2926
  .magicfeedback-error{
2829
2927
  margin-top: 5px;
2830
2928
  }`;
2929
+ }
2930
+ });
2831
2931
 
2832
2932
  // src/ui/renderPopup.ts
2933
+ var renderPopup_exports = {};
2934
+ __export(renderPopup_exports, {
2935
+ renderPopup: () => renderPopup
2936
+ });
2833
2937
  function ensureMagicFeedbackStyles(_popup) {
2834
2938
  const STYLE_ID = "magicfeedback-sdk-styles";
2835
2939
  if (document.getElementById(STYLE_ID)) return;
@@ -2887,7 +2991,7 @@ function ensureResponsiveStyles(_popup) {
2887
2991
  `;
2888
2992
  document.head.appendChild(style);
2889
2993
  }
2890
- async function renderPopup(container, surveyId, productId, actions, emit, onClose, env = "production", userId, style) {
2994
+ async function renderPopup(container, surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService) {
2891
2995
  let surveyCompletedEmitted = false;
2892
2996
  let stylesInjected = false;
2893
2997
  const isDark = style?.theme === "dark";
@@ -3198,8 +3302,8 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
3198
3302
  env: env === "production" ? "prod" : "dev"
3199
3303
  }
3200
3304
  );
3201
- const profile = userId ? [{ key: "external-user-id", value: [userId] }] : [];
3202
- formInstance = import_native.default.form(surveyId, productId, profile, profile);
3305
+ const { profile, metadata } = buildSurveyIdentity(userId ?? null, sessionId ?? null, miniService ?? null);
3306
+ formInstance = import_native.default.form(surveyId, productId, profile, metadata);
3203
3307
  const generateOptions = {
3204
3308
  addButton: false,
3205
3309
  getMetaData: true
@@ -3338,6 +3442,42 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
3338
3442
  setLoading(false);
3339
3443
  }
3340
3444
  }
3445
+ var import_native;
3446
+ var init_renderPopup = __esm({
3447
+ "src/ui/renderPopup.ts"() {
3448
+ "use strict";
3449
+ init_tracking_manager();
3450
+ import_native = __toESM(require_magicfeedback_sdk_browser());
3451
+ init_style();
3452
+ }
3453
+ });
3454
+
3455
+ // src/index.ts
3456
+ var index_exports = {};
3457
+ __export(index_exports, {
3458
+ BrowserPopupRenderer: () => BrowserPopupRenderer,
3459
+ ContactManager: () => ContactManager,
3460
+ DeepdotsPopups: () => DeepdotsPopups,
3461
+ InMemoryStorage: () => InMemoryStorage,
3462
+ NoopPopupRenderer: () => NoopPopupRenderer,
3463
+ POPUP_TRIGGER_CONDITION_STATUSES: () => POPUP_TRIGGER_CONDITION_STATUSES,
3464
+ ReactNativePopupRenderer: () => ReactNativePopupRenderer,
3465
+ buildAnalyticsFeedbackBody: () => buildAnalyticsFeedbackBody,
3466
+ buildSurveyHtml: () => buildSurveyHtml,
3467
+ collectRnDevice: () => collectRnDevice,
3468
+ createDefaultRenderer: () => createDefaultRenderer,
3469
+ createFeedbackSink: () => createFeedbackSink,
3470
+ mmkvStorage: () => mmkvStorage,
3471
+ setupReactNative: () => setupReactNative
3472
+ });
3473
+ module.exports = __toCommonJS(index_exports);
3474
+
3475
+ // src/types/index.ts
3476
+ var POPUP_TRIGGER_CONDITION_STATUSES = [
3477
+ "SHOWED",
3478
+ "PARTIAL",
3479
+ "COMPLETED"
3480
+ ];
3341
3481
 
3342
3482
  // src/triggers/index.ts
3343
3483
  function setupTrigger(popups, trigger) {
@@ -3516,41 +3656,125 @@ function resolveEnvironment(nodeEnv) {
3516
3656
  return ENVIRONMENT_MAP[nodeEnv || "production"];
3517
3657
  }
3518
3658
 
3659
+ // src/platform/react-native-renderer.ts
3660
+ init_tracking_manager();
3661
+
3662
+ // src/ui/surveyHtml.ts
3663
+ function buildSurveyHtml(opts) {
3664
+ const env = opts.env === "development" ? "dev" : "prod";
3665
+ const version = opts.version ?? "2.2.4";
3666
+ const cdn = `https://cdn.jsdelivr.net/npm/@magicfeedback/native@${version}/dist/magicfeedback-sdk.browser.js`;
3667
+ const sid = JSON.stringify(opts.surveyId);
3668
+ const pid = JSON.stringify(opts.productId);
3669
+ const profileJson = JSON.stringify(opts.profile ?? []);
3670
+ const metaJson = JSON.stringify(opts.metadata ?? []);
3671
+ return `<!DOCTYPE html><html><head>
3672
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
3673
+ <meta name="color-scheme" content="light"/>
3674
+ <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>
3675
+ </head><body>
3676
+ <div id="mf"></div>
3677
+ <script>
3678
+ (function(){
3679
+ function emit(s){ try{ if(window.ReactNativeWebView){ window.ReactNativeWebView.postMessage(s); } }catch(e){} }
3680
+ function emitJSON(n,p){ try{ emit(JSON.stringify({name:n,payload:p||{}})); }catch(e){} }
3681
+ window.DeepdotsClose = function(){ emitJSON('popup_close'); };
3682
+ var initialized=false;
3683
+ function initMF(){
3684
+ if(initialized || !window.magicfeedback) return initialized;
3685
+ initialized=true;
3686
+ try{
3687
+ window.magicfeedback.init({ debug:false, env:'${env}' });
3688
+ var form = window.magicfeedback.form(${sid}, ${pid}, ${profileJson}, ${metaJson});
3689
+ window.DeepdotsForm = form;
3690
+ form.generate('mf', {
3691
+ addButton:false,
3692
+ getMetaData:true,
3693
+ onLoadedEvent:function(){ emitJSON('loaded'); },
3694
+ beforeSubmitEvent:function(){ emitJSON('before_submit'); },
3695
+ afterSubmitEvent:function(p){
3696
+ try{ if(p && p.completed){ emitJSON('survey_completed', p); } else { emitJSON('after_submit', p); } }
3697
+ catch(e){ emitJSON('after_submit'); }
3698
+ },
3699
+ onBackEvent:function(p){ emitJSON('back', p); }
3700
+ }).catch(function(e){ emit('error:init'); });
3701
+ }catch(e){ emit('error:exception'); }
3702
+ return true;
3703
+ }
3704
+ var s=document.createElement('script'); s.src=${JSON.stringify(cdn)}; s.async=true;
3705
+ s.onload=function(){ initMF(); };
3706
+ s.onerror=function(){ emit('error:load'); };
3707
+ document.head.appendChild(s);
3708
+ var tries=0, t=setInterval(function(){ if(initMF()||++tries>40){ clearInterval(t); } },150);
3709
+ })();
3710
+ <\/script>
3711
+ </body></html>`;
3712
+ }
3713
+
3519
3714
  // src/platform/react-native-renderer.ts
3520
3715
  var ReactNativePopupRenderer = class {
3521
- constructor() {
3522
- this.mounted = false;
3523
- this.lastSurveyId = null;
3716
+ constructor(options = {}) {
3717
+ this.options = options;
3524
3718
  this.emitFn = null;
3525
3719
  this.onCloseFn = null;
3526
- this.completed = false;
3720
+ this.currentSurveyId = null;
3721
+ this.partialEmitted = false;
3527
3722
  }
3528
3723
  init() {
3529
3724
  }
3530
- show(surveyId, productId, data, emit, onClose) {
3531
- this.mounted = true;
3532
- this.lastSurveyId = surveyId;
3725
+ show(surveyId, productId, _actions, emit, onClose, env = "production", userId, _style, sessionId, miniService) {
3533
3726
  this.emitFn = emit;
3534
3727
  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;
3728
+ this.currentSurveyId = surveyId;
3729
+ this.partialEmitted = false;
3730
+ const { profile, metadata } = buildSurveyIdentity(userId ?? null, sessionId ?? null, miniService ?? null);
3731
+ const html = buildSurveyHtml({ surveyId, productId, env, profile, metadata });
3732
+ if (this.options.onShow) {
3733
+ this.options.onShow({ surveyId, productId, html });
3734
+ } else {
3735
+ console.warn(
3736
+ "[Deepdots] ReactNativePopupRenderer sin onShow: pasa { onShow } a new ReactNativePopupRenderer({...}) para montar el WebView del survey."
3737
+ );
3738
+ }
3548
3739
  }
3549
3740
  hide() {
3550
- if (!this.mounted) return;
3551
- this.mounted = false;
3552
- console.log("[ReactNativePopupRenderer] (stub) hide survey:", this.lastSurveyId);
3553
- this.lastSurveyId = null;
3741
+ this.options.onHide?.();
3742
+ this.currentSurveyId = null;
3743
+ }
3744
+ /**
3745
+ * El host la conecta al `onMessage` del WebView. Traduce los mensajes del survey a
3746
+ * eventos del SDK: primera interacción → `popup_clicked` (PARTIAL), completado →
3747
+ * `survey_completed` (COMPLETED) y cierra el popup.
3748
+ */
3749
+ handleMessage(raw) {
3750
+ const surveyId = this.currentSurveyId;
3751
+ if (!surveyId || !this.emitFn) return;
3752
+ let name;
3753
+ try {
3754
+ name = JSON.parse(raw).name;
3755
+ } catch {
3756
+ name = raw;
3757
+ }
3758
+ switch (name) {
3759
+ case "loaded":
3760
+ case "before_submit":
3761
+ case "after_submit":
3762
+ case "back":
3763
+ if (!this.partialEmitted) {
3764
+ this.partialEmitted = true;
3765
+ this.emitFn("popup_clicked", surveyId, { action: "partial" });
3766
+ }
3767
+ break;
3768
+ case "survey_completed":
3769
+ this.emitFn("survey_completed", surveyId);
3770
+ this.onCloseFn?.();
3771
+ break;
3772
+ case "popup_close":
3773
+ this.onCloseFn?.();
3774
+ break;
3775
+ default:
3776
+ break;
3777
+ }
3554
3778
  }
3555
3779
  };
3556
3780
  function isReactNativeEnv() {
@@ -3594,12 +3818,15 @@ var BrowserPopupRenderer = class {
3594
3818
  document.body.appendChild(this.container);
3595
3819
  }
3596
3820
  }
3597
- show(surveyId, productId, actions, emit, onClose, env = "production", userId, style) {
3821
+ show(surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService) {
3598
3822
  if (this.visible) return;
3599
3823
  if (!this.container || !document.body.contains(this.container)) this.init();
3600
3824
  if (!this.container) return;
3601
3825
  this.visible = true;
3602
- renderPopup(this.container, surveyId, productId, actions, emit, onClose, env, userId, style);
3826
+ const container = this.container;
3827
+ void Promise.resolve().then(() => (init_renderPopup(), renderPopup_exports)).then(({ renderPopup: renderPopup2 }) => {
3828
+ renderPopup2(container, surveyId, productId, actions, emit, onClose, env, userId, style, sessionId, miniService);
3829
+ });
3603
3830
  }
3604
3831
  hide() {
3605
3832
  this.visible = false;
@@ -3618,6 +3845,350 @@ function createDefaultRenderer() {
3618
3845
  return new BrowserPopupRenderer();
3619
3846
  }
3620
3847
 
3848
+ // src/core/deepdots-popups.ts
3849
+ init_tracking_manager();
3850
+
3851
+ // src/analytics/analytics-manager.ts
3852
+ var dryRunSink = (payload) => {
3853
+ console.log(
3854
+ "[DeepdotsAnalytics] (dry-run \xB7 NO enviado \xB7 sin init.analytics) POST /sdk/feedback \u2192",
3855
+ JSON.stringify(payload, null, 2)
3856
+ );
3857
+ };
3858
+ var AnalyticsManager = class {
3859
+ constructor(options = {}) {
3860
+ this.events = [];
3861
+ this.attributes = {};
3862
+ this.miniService = null;
3863
+ this.miniServiceEnteredAt = 0;
3864
+ this.sink = options.sink ?? dryRunSink;
3865
+ this.now = options.now ?? (() => Date.now());
3866
+ this.publicKey = options.publicKey;
3867
+ this.language = options.language;
3868
+ this.device = options.device;
3869
+ this.platform = options.platform ?? "web";
3870
+ }
3871
+ /** Mezcla user attributes (se coercionan a string). Mutable en runtime. */
3872
+ setUserAttributes(attrs) {
3873
+ for (const [k, v] of Object.entries(attrs)) {
3874
+ if (!k) continue;
3875
+ this.attributes[k] = String(v);
3876
+ }
3877
+ }
3878
+ /** Marca el inicio de un mini-service; etiqueta los eventos siguientes con `mini_service`. */
3879
+ enterMiniService(name, entryPointType) {
3880
+ this.miniService = name;
3881
+ this.miniServiceEnteredAt = this.now();
3882
+ this.track("mini_service_enter", { entry_point_type: entryPointType ?? null });
3883
+ }
3884
+ /** Cierra el mini-service activo emitiendo `mini_service_exit` con su duración (#27). No-op si no hay ninguno. */
3885
+ exitMiniService() {
3886
+ const name = this.miniService;
3887
+ if (name == null) return;
3888
+ const durationSeconds = Math.max(0, Math.round((this.now() - this.miniServiceEnteredAt) / 1e3));
3889
+ this.miniService = null;
3890
+ this.track("mini_service_exit", { mini_service: name, duration_seconds: durationSeconds });
3891
+ }
3892
+ /** Registra un evento de analítica (modelo GA: nombre + params). */
3893
+ track(name, params) {
3894
+ const merged = {
3895
+ ...this.miniService ? { mini_service: this.miniService } : {},
3896
+ ...params ?? {}
3897
+ };
3898
+ this.events.push({
3899
+ name,
3900
+ timestamp: this.now(),
3901
+ params: Object.keys(merged).length ? merged : void 0
3902
+ });
3903
+ }
3904
+ /** Mini-service activo (para inyectarlo en la metadata del survey, #33). */
3905
+ getMiniService() {
3906
+ return this.miniService;
3907
+ }
3908
+ /** Nº de eventos pendientes de flush. */
3909
+ pending() {
3910
+ return this.events.length;
3911
+ }
3912
+ /** Construye el envelope que se enviaría al endpoint de analytics. */
3913
+ buildPayload(identity) {
3914
+ return {
3915
+ publicKey: this.publicKey,
3916
+ userId: identity.userId,
3917
+ sessionId: identity.sessionId,
3918
+ context: {
3919
+ platform: this.platform,
3920
+ language: this.language,
3921
+ device: this.device,
3922
+ attributes: { ...this.attributes }
3923
+ },
3924
+ events: [...this.events]
3925
+ };
3926
+ }
3927
+ /** Envía (vía sink) el lote acumulado y vacía el buffer. No-op si no hay eventos. */
3928
+ flush(identity) {
3929
+ if (this.events.length === 0) return null;
3930
+ const payload = this.buildPayload(identity);
3931
+ this.sink(payload);
3932
+ this.events = [];
3933
+ return payload;
3934
+ }
3935
+ };
3936
+
3937
+ // src/analytics/feedback-payload.ts
3938
+ function pushKV(list, key, value) {
3939
+ if (value === void 0 || value === null || value === "") return;
3940
+ list.push({ key, value: String(value) });
3941
+ }
3942
+ function buildAnalyticsFeedbackBody(envelope, keys, feedbackSessionId) {
3943
+ const { context } = envelope;
3944
+ const metrics = envelope.events.map((e) => ({
3945
+ key: e.name,
3946
+ value: JSON.stringify({ timestamp: e.timestamp, ...e.params ?? {} })
3947
+ }));
3948
+ const profile = [];
3949
+ pushKV(profile, "external-user-id", envelope.userId);
3950
+ const metadata = [];
3951
+ pushKV(metadata, "user_id", envelope.userId);
3952
+ pushKV(metadata, "session_id", envelope.sessionId);
3953
+ pushKV(metadata, "platform", context.platform);
3954
+ pushKV(metadata, "language", context.language);
3955
+ if (context.device) {
3956
+ pushKV(metadata, "device_type", context.device.device_type);
3957
+ pushKV(metadata, "os_version", context.device.os_version);
3958
+ pushKV(metadata, "device_model", context.device.device_model);
3959
+ pushKV(metadata, "app_version", context.device.app_version);
3960
+ pushKV(metadata, "user_agent", context.device.user_agent);
3961
+ }
3962
+ for (const [k, v] of Object.entries(context.attributes ?? {})) {
3963
+ pushKV(metadata, k, v);
3964
+ }
3965
+ return {
3966
+ feedback: {
3967
+ text: "",
3968
+ answers: [],
3969
+ metadata,
3970
+ metrics,
3971
+ profile,
3972
+ finished: false
3973
+ },
3974
+ publicKey: keys.publicKey,
3975
+ integration: keys.integration,
3976
+ completed: false,
3977
+ // finished: false,
3978
+ ...feedbackSessionId ? { sessionId: feedbackSessionId } : {}
3979
+ };
3980
+ }
3981
+ function createFeedbackSink(options) {
3982
+ let feedbackSessionId;
3983
+ return (envelope) => {
3984
+ const body = buildAnalyticsFeedbackBody(envelope, options.keys, feedbackSessionId);
3985
+ const f = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
3986
+ if (!f) {
3987
+ options.log?.("[DeepdotsAnalytics] no fetch disponible; payload no enviado", body);
3988
+ return;
3989
+ }
3990
+ options.log?.("[DeepdotsAnalytics] POST /sdk/feedback \u2192", body);
3991
+ f(`${options.baseUrl}/sdk/feedback`, {
3992
+ method: "POST",
3993
+ headers: { "Content-Type": "application/json" },
3994
+ body: JSON.stringify(body)
3995
+ }).then(async (res) => {
3996
+ if (res.ok) {
3997
+ try {
3998
+ const data = await res.json();
3999
+ if (data?.sessionId && data.sessionId !== feedbackSessionId) {
4000
+ feedbackSessionId = data.sessionId;
4001
+ options.log?.("[DeepdotsAnalytics] feedbackSessionId cacheado:", feedbackSessionId);
4002
+ }
4003
+ } catch {
4004
+ }
4005
+ }
4006
+ }).catch((err) => options.log?.("[DeepdotsAnalytics] error enviando feedback", err));
4007
+ };
4008
+ }
4009
+
4010
+ // src/tracking/navigation-observer.ts
4011
+ var UUID_PREFIX = /^[0-9a-f]{8}-[0-9a-f]{4}-/i;
4012
+ function normalizeScreen(href) {
4013
+ let pathAndHash;
4014
+ try {
4015
+ const u = new URL(href, "http://deepdots.local");
4016
+ pathAndHash = u.pathname + (u.hash || "");
4017
+ } catch {
4018
+ pathAndHash = href;
4019
+ }
4020
+ pathAndHash = pathAndHash.replace(/\?[^#]*/g, "");
4021
+ const collapsed = pathAndHash.split("/").map((seg) => /^[0-9]+$/.test(seg) || UUID_PREFIX.test(seg) ? ":id" : seg).join("/");
4022
+ return collapsed || "/";
4023
+ }
4024
+ var NavigationObserver = class {
4025
+ constructor(options = {}) {
4026
+ this.listeners = /* @__PURE__ */ new Set();
4027
+ this.currentScreen = null;
4028
+ this.entryAt = 0;
4029
+ this.installed = false;
4030
+ this.domHandlers = [];
4031
+ this.now = options.now ?? (() => Date.now());
4032
+ }
4033
+ onVisit(listener) {
4034
+ this.listeners.add(listener);
4035
+ }
4036
+ /** Fija la pantalla inicial (sin emitir visita; su duración se cierra al salir). */
4037
+ begin(href) {
4038
+ this.currentScreen = normalizeScreen(href);
4039
+ this.entryAt = this.now();
4040
+ }
4041
+ /** Navega a una pantalla nueva: cierra la anterior (emite visita) y abre la nueva. */
4042
+ visit(href) {
4043
+ const next = normalizeScreen(href);
4044
+ if (next === this.currentScreen) return;
4045
+ this.closeCurrent();
4046
+ this.currentScreen = next;
4047
+ this.entryAt = this.now();
4048
+ }
4049
+ /** Cierra la pantalla actual emitiendo su visita (p. ej. al cerrar la app/pestaña). */
4050
+ stop() {
4051
+ this.closeCurrent();
4052
+ this.currentScreen = null;
4053
+ }
4054
+ closeCurrent() {
4055
+ if (this.currentScreen == null) return;
4056
+ const exit = this.now();
4057
+ const visit = {
4058
+ screen: this.currentScreen,
4059
+ entry: this.entryAt,
4060
+ exit,
4061
+ durationSeconds: Math.max(0, Math.round((exit - this.entryAt) / 1e3))
4062
+ };
4063
+ this.listeners.forEach((l) => {
4064
+ try {
4065
+ l(visit);
4066
+ } catch {
4067
+ }
4068
+ });
4069
+ }
4070
+ /** Instala los hooks de History en el navegador y registra la pantalla actual. */
4071
+ install() {
4072
+ if (this.installed || typeof window === "undefined" || typeof history === "undefined") return;
4073
+ this.installed = true;
4074
+ const handle = () => this.visit(window.location.href);
4075
+ const originalPush = history.pushState.bind(history);
4076
+ const originalReplace = history.replaceState.bind(history);
4077
+ history.pushState = (data, unused, url) => {
4078
+ const r = originalPush(data, unused, url);
4079
+ handle();
4080
+ return r;
4081
+ };
4082
+ history.replaceState = (data, unused, url) => {
4083
+ const r = originalReplace(data, unused, url);
4084
+ handle();
4085
+ return r;
4086
+ };
4087
+ window.addEventListener("popstate", handle);
4088
+ window.addEventListener("hashchange", handle);
4089
+ this.domHandlers.push(() => {
4090
+ history.pushState = originalPush;
4091
+ history.replaceState = originalReplace;
4092
+ window.removeEventListener("popstate", handle);
4093
+ window.removeEventListener("hashchange", handle);
4094
+ });
4095
+ this.begin(window.location.href);
4096
+ }
4097
+ uninstall() {
4098
+ this.domHandlers.forEach((off) => off());
4099
+ this.domHandlers = [];
4100
+ this.installed = false;
4101
+ }
4102
+ };
4103
+
4104
+ // src/analytics/device-info.ts
4105
+ function parseDeviceType(ua) {
4106
+ const s = (ua || "").toLowerCase();
4107
+ if (/ipad|tablet|playbook|silk/.test(s) || /android/.test(s) && !/mobile/.test(s)) {
4108
+ return "tablet";
4109
+ }
4110
+ if (/mobi|iphone|ipod|windows phone/.test(s) || /android/.test(s) && /mobile/.test(s)) {
4111
+ return "mobile";
4112
+ }
4113
+ return "desktop";
4114
+ }
4115
+ function collectDeviceInfo(appVersion) {
4116
+ const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
4117
+ return {
4118
+ device_type: parseDeviceType(ua),
4119
+ user_agent: ua || void 0,
4120
+ app_version: appVersion
4121
+ };
4122
+ }
4123
+
4124
+ // src/analytics/engagement-tracker.ts
4125
+ var EngagementTracker = class {
4126
+ constructor(now = () => Date.now()) {
4127
+ this.activeMs = 0;
4128
+ this.lastResumeAt = null;
4129
+ this.now = now;
4130
+ }
4131
+ /** Reanuda el conteo (pestaña visible / app en foreground). Idempotente. */
4132
+ resume() {
4133
+ if (this.lastResumeAt === null) this.lastResumeAt = this.now();
4134
+ }
4135
+ /** Pausa el conteo (pestaña oculta / app en background), acumulando el tramo activo. */
4136
+ pause() {
4137
+ if (this.lastResumeAt !== null) {
4138
+ this.activeMs += this.now() - this.lastResumeAt;
4139
+ this.lastResumeAt = null;
4140
+ }
4141
+ }
4142
+ /** Devuelve los ms activos acumulados desde la última lectura y reinicia (sin parar el timer). */
4143
+ consume() {
4144
+ if (this.lastResumeAt !== null) {
4145
+ const t = this.now();
4146
+ this.activeMs += t - this.lastResumeAt;
4147
+ this.lastResumeAt = t;
4148
+ }
4149
+ const ms = this.activeMs;
4150
+ this.activeMs = 0;
4151
+ return ms;
4152
+ }
4153
+ isActive() {
4154
+ return this.lastResumeAt !== null;
4155
+ }
4156
+ };
4157
+
4158
+ // src/contact/contact-manager.ts
4159
+ init_tracking_manager();
4160
+ function stableSerialize(attrs) {
4161
+ const sorted = {};
4162
+ for (const key of Object.keys(attrs).sort()) {
4163
+ sorted[key] = attrs[key];
4164
+ }
4165
+ return JSON.stringify(sorted);
4166
+ }
4167
+ var ContactManager = class {
4168
+ constructor(options) {
4169
+ this.storage = options.storage;
4170
+ this.publicKey = options.publicKey;
4171
+ this.userId = options.userId;
4172
+ this.post = options.post;
4173
+ }
4174
+ /**
4175
+ * Envía los atributos si cambiaron respecto a lo último enviado.
4176
+ * @returns `true` si se envió, `false` si no hubo cambios.
4177
+ */
4178
+ async setAttributes(attributes) {
4179
+ const serialized = stableSerialize(attributes);
4180
+ const last = this.storage.getItem(STORAGE_KEYS.contactAttributes);
4181
+ if (last === serialized) return false;
4182
+ await this.post({
4183
+ publicKey: this.publicKey,
4184
+ userId: this.userId,
4185
+ userAttributes: attributes
4186
+ });
4187
+ this.storage.setItem(STORAGE_KEYS.contactAttributes, serialized);
4188
+ return true;
4189
+ }
4190
+ };
4191
+
3621
4192
  // src/core/deepdots-popups.ts
3622
4193
  var EXIT_QUEUE_STORAGE_KEY = "__deepdots_exit_popup_queue__";
3623
4194
  var DAY_IN_MS = 24 * 60 * 60 * 1e3;
@@ -3642,6 +4213,18 @@ var DeepdotsPopups = class {
3642
4213
  this.deferredExitTimers = [];
3643
4214
  this.baseUrl = "";
3644
4215
  this.env = "production";
4216
+ /** Identidad + sesión (Fase 1 tracking). Null hasta init(). */
4217
+ this.tracking = null;
4218
+ /** Capa de analytics (canal separado del feedback). Null hasta init(). */
4219
+ this.analytics = null;
4220
+ /** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
4221
+ this.navObserver = null;
4222
+ /** Tiempo activo (engagement time, #8). */
4223
+ this.engagement = null;
4224
+ /** Atributos internos del usuario identificado → POST /sdk/popups/contact. Null si no hay userId. */
4225
+ this.contact = null;
4226
+ /** Marca si ya se inició la navegación manual (setScreen) — para RN sin History API. */
4227
+ this.navStarted = false;
3645
4228
  }
3646
4229
  /** Initialize the SDK with configuration */
3647
4230
  init(config) {
@@ -3654,33 +4237,196 @@ var DeepdotsPopups = class {
3654
4237
  this.env = config.nodeEnv || "production";
3655
4238
  this.config = {
3656
4239
  apiKey: config.apiKey || void 0,
3657
- mode: config.mode || "client",
3658
4240
  debug: config.debug || false,
3659
- popups: config.popups || [],
3660
4241
  userId: config.userId || void 0
3661
4242
  };
4243
+ const storage = config.storage ?? createDefaultStorage();
4244
+ this.tracking = new TrackingManager({
4245
+ storage,
4246
+ clientUserId: this.config.userId,
4247
+ enabled: config.trackingEnabled ?? true
4248
+ });
4249
+ if (this.config.userId) {
4250
+ const userId = this.config.userId;
4251
+ const publicKey = this.config.apiKey ?? "";
4252
+ this.contact = new ContactManager({
4253
+ storage,
4254
+ publicKey,
4255
+ userId,
4256
+ post: (body) => this.postContact(body)
4257
+ });
4258
+ if (config.contactAttributes) {
4259
+ void this.setContactAttributes(config.contactAttributes);
4260
+ }
4261
+ }
4262
+ this.log("tracking \xB7 user_id:", this.tracking.getUserId(), "\xB7 new_user:", this.tracking.isNewUser(), "\xB7 enabled:", this.tracking.isTrackingEnabled());
4263
+ const analyticsSink = config.analytics ? createFeedbackSink({
4264
+ baseUrl: this.baseUrl,
4265
+ keys: config.analytics,
4266
+ log: (...a) => this.log(...a)
4267
+ }) : void 0;
4268
+ this.analytics = new AnalyticsManager({
4269
+ sink: analyticsSink,
4270
+ publicKey: config.analytics?.publicKey ?? this.config.apiKey,
4271
+ language: typeof navigator !== "undefined" ? navigator.language : void 0,
4272
+ platform: config.platform ?? "web",
4273
+ device: config.device ?? collectDeviceInfo(config.appVersion)
4274
+ });
4275
+ this.navObserver = new NavigationObserver();
4276
+ this.navObserver.onVisit((v) => this.track("page_view", { screen: v.screen, duration_seconds: v.durationSeconds }));
4277
+ this.navObserver.install();
4278
+ this.engagement = new EngagementTracker();
4279
+ this.engagement.resume();
4280
+ this.setupAnalyticsFlush();
3662
4281
  this.initialized = true;
3663
4282
  this.log("SDK initialized", this.config);
3664
4283
  if (this.renderer.init) this.renderer.init();
3665
4284
  this.setupPopupContainer();
3666
- if (this.config.mode === "client") {
3667
- this.popupDefinitions = this.validatePopupDefinitions(this.config.popups || []);
4285
+ this.fetchPopupsFromServer().then((defs) => {
4286
+ this.popupDefinitions = this.validatePopupDefinitions(defs);
3668
4287
  this.popupsLoaded = true;
4288
+ this.log("Popups loaded from API", this.popupDefinitions);
3669
4289
  this.configureTriggersFromDefinitions();
3670
4290
  this.processDeferredExitQueue();
4291
+ if (this.pendingAutoLaunch) {
4292
+ this.startTriggers();
4293
+ }
4294
+ });
4295
+ }
4296
+ /** User id actual (generado por el SDK o provisto por el cliente). Null si tracking off. */
4297
+ getUserId() {
4298
+ return this.tracking?.getUserId() ?? null;
4299
+ }
4300
+ /** Session id de navegación actual. Null si tracking off. */
4301
+ getSessionId() {
4302
+ return this.tracking?.getSessionId() ?? null;
4303
+ }
4304
+ /** Activa/desactiva el tracking (identidad + sesión + analytics). Kill-switch del contrato §7bis. */
4305
+ setTrackingEnabled(enabled) {
4306
+ this.tracking?.setTrackingEnabled(enabled);
4307
+ this.log("tracking \xB7 setTrackingEnabled:", enabled, "\xB7 session_id:", this.tracking?.getSessionId() ?? null);
4308
+ }
4309
+ // ───────── Analytics (canal separado del feedback, vinculado por user_id) ─────────
4310
+ /** Registra un evento de analítica (modelo GA: nombre + params). No-op si tracking off. */
4311
+ track(name, params) {
4312
+ if (!this.tracking?.isTrackingEnabled()) return;
4313
+ this.analytics?.track(name, params);
4314
+ this.log("analytics \xB7 track:", name, params ?? {});
4315
+ }
4316
+ /** User attributes para breakdowns (registration_status, pass_type, sector, pass_status…). */
4317
+ setUserAttributes(attributes) {
4318
+ if (!this.tracking?.isTrackingEnabled()) return;
4319
+ this.analytics?.setUserAttributes(attributes);
4320
+ }
4321
+ /**
4322
+ * Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
4323
+ * Contact del backend para segmentación/targeting de popups. Identificada por el `userId` del
4324
+ * init. Solo envía si los atributos cambiaron (diff en storage). No-op si tracking off o sin userId.
4325
+ * @returns `true` si se envió al backend, `false` si no hubo cambios o está deshabilitado.
4326
+ */
4327
+ async setContactAttributes(attributes) {
4328
+ if (!this.tracking?.isTrackingEnabled()) return false;
4329
+ if (!this.contact) return false;
4330
+ return this.contact.setAttributes(attributes);
4331
+ }
4332
+ /** Marca el inicio de un mini-service; etiqueta los eventos siguientes. No-op si tracking off. */
4333
+ enterMiniService(name, entryPointType) {
4334
+ if (!this.tracking?.isTrackingEnabled()) return;
4335
+ this.analytics?.enterMiniService(name, entryPointType);
4336
+ this.log("analytics \xB7 enterMiniService:", name, entryPointType ?? "");
4337
+ }
4338
+ /** Cierra el mini-service activo (emite `mini_service_exit` con duración, #27). No-op si tracking off. */
4339
+ exitMiniService() {
4340
+ if (!this.tracking?.isTrackingEnabled()) return;
4341
+ this.analytics?.exitMiniService();
4342
+ this.log("analytics \xB7 exitMiniService");
4343
+ }
4344
+ /** Findability (#31/#35): registra una búsqueda. `has_results` se deriva de `resultsCount`. */
4345
+ trackSearch(query, resultsCount, params) {
4346
+ this.track("search", { query, results_count: resultsCount, has_results: resultsCount > 0, ...params ?? {} });
4347
+ }
4348
+ /** Findability friction (#34/#35): señal de fricción con su `friction_topic`. */
4349
+ trackFindabilityFriction(frictionTopic, params) {
4350
+ this.track("findability_friction", { friction_topic: frictionTopic, ...params ?? {} });
4351
+ }
4352
+ /** Funnel: un paso del embudo, correlacionado por `taskId`. El backend reconstruye conversión/drop-off/tiempo. */
4353
+ trackFunnelStep(funnel, step, taskId, params) {
4354
+ this.track("funnel_step", { funnel, step, task_id: taskId, ...params ?? {} });
4355
+ }
4356
+ /**
4357
+ * Navegación MANUAL (entornos sin History API, p. ej. React Native con React Navigation).
4358
+ * El host la llama en cada cambio de pantalla; emite `page_view` al salir de la anterior.
4359
+ */
4360
+ setScreen(name) {
4361
+ if (!this.navObserver) return;
4362
+ if (!this.navStarted) {
4363
+ this.navObserver.begin(name);
4364
+ this.navStarted = true;
3671
4365
  } 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
- });
4366
+ this.navObserver.visit(name);
3682
4367
  }
3683
4368
  }
4369
+ /** App a foreground (RN: AppState 'active'): reanuda el engagement time. */
4370
+ onForeground() {
4371
+ this.engagement?.resume();
4372
+ }
4373
+ /**
4374
+ * App a background (RN: AppState 'background'): cierra pantalla actual (page_view),
4375
+ * mini-service (mini_service_exit), emite engagement y hace flush del lote.
4376
+ */
4377
+ onBackground() {
4378
+ this.navObserver?.stop();
4379
+ this.navStarted = false;
4380
+ this.exitMiniService();
4381
+ this.flushEngagement();
4382
+ this.engagement?.pause();
4383
+ this.flushAnalytics();
4384
+ }
4385
+ /** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
4386
+ previewAnalytics() {
4387
+ return this.analytics?.buildPayload(this.analyticsIdentity()) ?? {
4388
+ userId: null,
4389
+ sessionId: null,
4390
+ context: { platform: "web", attributes: {} },
4391
+ events: []
4392
+ };
4393
+ }
4394
+ /** Envía (hoy dry-run → console.log) el lote acumulado de analytics y vacía el buffer. */
4395
+ flushAnalytics() {
4396
+ if (!this.tracking?.isTrackingEnabled()) return;
4397
+ this.analytics?.flush(this.analyticsIdentity());
4398
+ }
4399
+ /** Emite un evento `user_engagement` con el tiempo activo acumulado (#8). */
4400
+ flushEngagement() {
4401
+ if (!this.tracking?.isTrackingEnabled()) return;
4402
+ const ms = this.engagement?.consume() ?? 0;
4403
+ if (ms > 0) this.track("user_engagement", { engagement_time_msec: ms });
4404
+ }
4405
+ analyticsIdentity() {
4406
+ return {
4407
+ userId: this.tracking?.getUserId() ?? null,
4408
+ sessionId: this.tracking?.getSessionId() ?? null
4409
+ };
4410
+ }
4411
+ /** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
4412
+ setupAnalyticsFlush() {
4413
+ if (typeof document === "undefined" || typeof window === "undefined") return;
4414
+ window.addEventListener("pagehide", () => {
4415
+ this.navObserver?.stop();
4416
+ this.exitMiniService();
4417
+ this.flushEngagement();
4418
+ this.flushAnalytics();
4419
+ });
4420
+ document.addEventListener("visibilitychange", () => {
4421
+ if (document.visibilityState === "hidden") {
4422
+ this.flushEngagement();
4423
+ this.engagement?.pause();
4424
+ this.flushAnalytics();
4425
+ } else if (document.visibilityState === "visible") {
4426
+ this.engagement?.resume();
4427
+ }
4428
+ });
4429
+ }
3684
4430
  /** Enable auto-launch functionality with configured triggers */
3685
4431
  autoLaunch() {
3686
4432
  if (!this.initialized) {
@@ -3698,35 +4444,6 @@ var DeepdotsPopups = class {
3698
4444
  this.log("Auto-launch enabled");
3699
4445
  this.triggers.forEach((trigger) => setupTrigger(this, trigger));
3700
4446
  }
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
4447
  /** Configure triggers for auto-launching popups (manual) */
3731
4448
  configureTriggers(triggers) {
3732
4449
  if (!this.initialized) {
@@ -3795,7 +4512,11 @@ var DeepdotsPopups = class {
3795
4512
  this.showDefinition(matched);
3796
4513
  }
3797
4514
  showDefinition(def) {
3798
- this.show(def);
4515
+ this.log("Showing popup (definition)", def);
4516
+ this.surveyToPopupId.set(def.surveyId, def.id);
4517
+ this.lastShown.set(def.id, Date.now());
4518
+ this.renderPopup(def.surveyId, def.productId, def.actions, def.style);
4519
+ this.emitEvent("popup_shown", def.surveyId, { popupId: def.id });
3799
4520
  }
3800
4521
  shouldShow(def, pathUrl, skipPathCheck = false) {
3801
4522
  if (!skipPathCheck && !this.matchesSegmentsPath(def, pathUrl)) {
@@ -4039,6 +4760,26 @@ var DeepdotsPopups = class {
4039
4760
  return [];
4040
4761
  }
4041
4762
  }
4763
+ /** Envía los atributos de contact a la API (`POST /sdk/popups/contact`). */
4764
+ async postContact(body) {
4765
+ const baseUrl = this.baseUrl;
4766
+ if (!baseUrl) return;
4767
+ const endpoint = `${baseUrl}/sdk/popups/contact`;
4768
+ try {
4769
+ const response = await fetch(endpoint, {
4770
+ method: "POST",
4771
+ headers: { "Content-Type": "application/json" },
4772
+ body: JSON.stringify(body)
4773
+ });
4774
+ if (!response.ok) {
4775
+ this.log("Failed to post contact", response.status, response.statusText);
4776
+ return;
4777
+ }
4778
+ this.log("contact \xB7 sent attributes for user:", body.userId);
4779
+ } catch (error) {
4780
+ this.log("Error posting contact", error);
4781
+ }
4782
+ }
4042
4783
  /** Notifica eventos de popup a la API */
4043
4784
  async postPopupEvent(status, popupId, userId) {
4044
4785
  const apiKey = this.config?.apiKey;
@@ -4060,6 +4801,15 @@ var DeepdotsPopups = class {
4060
4801
  });
4061
4802
  if (!response.ok) {
4062
4803
  this.log("Failed to post popup event", response.status, response.statusText);
4804
+ return;
4805
+ }
4806
+ try {
4807
+ const data = await response.json();
4808
+ if (data && typeof data.sessionId === "string") {
4809
+ this.tracking?.setSessionId(data.sessionId);
4810
+ this.log("tracking \xB7 session_id from backend:", data.sessionId);
4811
+ }
4812
+ } catch {
4063
4813
  }
4064
4814
  } catch (error) {
4065
4815
  this.log("Error posting popup event", error);
@@ -4102,7 +4852,10 @@ var DeepdotsPopups = class {
4102
4852
  }
4103
4853
  /** Render the popup UI */
4104
4854
  renderPopup(surveyId, productId, actions, style) {
4105
- const userId = this.config?.userId;
4855
+ const userId = this.tracking?.getUserId() ?? this.config?.userId;
4856
+ const sessionId = this.tracking?.getSessionId() ?? void 0;
4857
+ const miniService = this.analytics?.getMiniService() ?? void 0;
4858
+ this.log("tracking \xB7 survey identity \u2192", { userId: userId ?? null, sessionId: sessionId ?? null, miniService: miniService ?? null });
4106
4859
  if (this.renderer) {
4107
4860
  this.renderer.show(
4108
4861
  surveyId,
@@ -4112,22 +4865,29 @@ var DeepdotsPopups = class {
4112
4865
  () => this.hidePopup(),
4113
4866
  this.env,
4114
4867
  userId,
4115
- style
4868
+ style,
4869
+ sessionId,
4870
+ miniService
4116
4871
  );
4117
4872
  return;
4118
4873
  }
4119
4874
  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
- );
4875
+ const container = this.popupContainer;
4876
+ void Promise.resolve().then(() => (init_renderPopup(), renderPopup_exports)).then(({ renderPopup: renderPopup2 }) => {
4877
+ renderPopup2(
4878
+ container,
4879
+ surveyId,
4880
+ productId,
4881
+ actions,
4882
+ (type, id, payload) => this.emitEvent(type, id, payload),
4883
+ () => this.hidePopup(),
4884
+ this.env,
4885
+ userId,
4886
+ style,
4887
+ sessionId,
4888
+ miniService
4889
+ );
4890
+ });
4131
4891
  }
4132
4892
  /** Hide the popup */
4133
4893
  hidePopup() {
@@ -4144,18 +4904,20 @@ var DeepdotsPopups = class {
4144
4904
  emitEvent(type, surveyId, data) {
4145
4905
  const event = { type, surveyId, timestamp: Date.now(), data };
4146
4906
  this.log("Event emitted", event);
4147
- if (type === "popup_clicked" && data?.action === "partial") {
4907
+ const isPartialClick = type === "popup_clicked" && data?.action === "partial";
4908
+ if (isPartialClick) {
4148
4909
  this.markSurveyProgress(surveyId, "PARTIAL");
4149
4910
  }
4150
4911
  if (type === "survey_completed") {
4151
4912
  this.markSurveyAnswered(surveyId);
4152
4913
  }
4153
- if (type === "popup_shown" || type === "survey_completed") {
4914
+ if (type === "popup_shown" || type === "survey_completed" || isPartialClick) {
4154
4915
  const popupIdFromData = data?.popupId;
4155
4916
  const popupId = popupIdFromData || this.surveyToPopupId.get(surveyId);
4156
4917
  if (popupId) {
4157
4918
  const userIdFromData = data?.userId;
4158
- void this.postPopupEvent(type === "popup_shown" ? "SHOWED" /* SHOWED */ : "COMPLETED" /* COMPLETED */, popupId, userIdFromData || this.config?.userId);
4919
+ const status = type === "popup_shown" ? "SHOWED" /* SHOWED */ : type === "survey_completed" ? "COMPLETED" /* COMPLETED */ : "PARTIAL" /* PARTIAL */;
4920
+ void this.postPopupEvent(status, popupId, userIdFromData || this.tracking?.getUserId() || this.config?.userId);
4159
4921
  } else {
4160
4922
  this.debug("No popupId available to post event", { type, surveyId });
4161
4923
  }
@@ -4291,4 +5053,41 @@ var DeepdotsPopups = class {
4291
5053
  return defs.map((def) => this.normalizePopupDefinition(def)).filter((def) => !!def && this.isPopupDefinition(def));
4292
5054
  }
4293
5055
  };
5056
+
5057
+ // src/index.ts
5058
+ init_tracking_manager();
5059
+
5060
+ // src/react-native/setup.ts
5061
+ function mmkvStorage(mmkv) {
5062
+ return {
5063
+ getItem: (k) => mmkv.getString(k) ?? null,
5064
+ setItem: (k, v) => mmkv.set(k, v),
5065
+ removeItem: (k) => mmkv.delete(k)
5066
+ };
5067
+ }
5068
+ function collectRnDevice(d) {
5069
+ return {
5070
+ device_type: d.isTablet?.() ? "tablet" : "mobile",
5071
+ os_version: d.getSystemVersion?.(),
5072
+ device_model: d.getDeviceId?.(),
5073
+ app_version: d.getVersion?.()
5074
+ };
5075
+ }
5076
+ function setupReactNative(sdk, config, deps = {}) {
5077
+ if (deps.renderer) sdk.setRenderer(deps.renderer);
5078
+ const storage = deps.mmkv ? mmkvStorage(deps.mmkv) : config.storage;
5079
+ const device = deps.deviceInfo ? collectRnDevice(deps.deviceInfo) : config.device;
5080
+ const platform = deps.platform ?? config.platform;
5081
+ sdk.init({ ...config, storage, device, platform });
5082
+ let subscription;
5083
+ if (deps.appState?.addEventListener) {
5084
+ subscription = deps.appState.addEventListener("change", (state) => {
5085
+ if (state === "active") sdk.onForeground();
5086
+ else sdk.onBackground();
5087
+ });
5088
+ }
5089
+ return () => {
5090
+ if (subscription && typeof subscription.remove === "function") subscription.remove();
5091
+ };
5092
+ }
4294
5093
  //# sourceMappingURL=index.js.map