@magicfeedback/popup-sdk 1.8.5 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -122,6 +122,19 @@ interface DeepdotsLogger {
122
122
  error?: (...args: unknown[]) => void;
123
123
  info?: (...args: unknown[]) => void;
124
124
  }
125
+ /**
126
+ * A session of the analytics channel as the Deepdots API sees it. Every session becomes ONE
127
+ * feedback, and `sessionId` is what the API stores as `sdkSessionId` on it: the value to look
128
+ * that feedback up with (`GET /feedbacks?filter={"where":{"sdkSessionId":"<sessionId>"}}`).
129
+ * A session that closes again keeps that one feedback: the API updates it.
130
+ * - `open`: the API acknowledged the session's first batch; its events keep accumulating.
131
+ * - `closed`: the API acknowledged the session's closing batch (`completed: true`); the
132
+ * feedback is created from it within seconds.
133
+ */
134
+ interface FeedbackSession {
135
+ sessionId: string;
136
+ status: 'open' | 'closed';
137
+ }
125
138
  interface DeepdotsInitParams {
126
139
  /** API key for authentication */
127
140
  apiKey?: string;
@@ -157,6 +170,17 @@ interface DeepdotsInitParams {
157
170
  * logged when `debug` is true.
158
171
  */
159
172
  analytics?: AnalyticsKeys;
173
+ /**
174
+ * Called when the analytics channel opens or closes a session (needs `analytics`), with the
175
+ * id the Deepdots API stores as `sdkSessionId` on the feedback the session becomes: send it
176
+ * to your backend to attach data to that feedback later. A session whose only batch is the
177
+ * closing one reports just `closed`. `closed` does not come when the page unloads before the
178
+ * closing request returns, for a session dropped after 30 min hidden, nor when the closing
179
+ * request fails (the API closes those itself, later): their id already came with `open`. Key
180
+ * by `sessionId`, not by order: the next session's `open` can arrive before the previous
181
+ * one's `closed`. Same value as `getFeedbackSessionId()` while the session is open.
182
+ */
183
+ onFeedbackSession?: (session: FeedbackSession) => void;
160
184
  /**
161
185
  * IP geolocation (country/city) added to analytics. Default `true`. The lookup calls
162
186
  * third-party services (ipapi.co, then ipwho.is, then ipinfo.io as fallbacks, 3 s timeout
@@ -392,6 +416,8 @@ declare class DeepdotsPopups {
392
416
  private analytics;
393
417
  /** feedbackSessionId cacheado del canal de analytics (devuelto por POST /sdk/feedback). */
394
418
  private analyticsFeedbackSessionId;
419
+ /** `init({ onFeedbackSession })`: avisa al host al abrir y cerrar cada registro. */
420
+ private onFeedbackSession;
395
421
  /** Timer del flush periódico de analytics (cada ANALYTICS_FLUSH_INTERVAL_MS). */
396
422
  private analyticsFlushTimer;
397
423
  /** Crash & error reporting (#14–17). Null hasta init(). */
@@ -435,6 +461,15 @@ declare class DeepdotsPopups {
435
461
  getUserId(): string | null;
436
462
  /** Session id de navegación actual. Null si tracking off. */
437
463
  getSessionId(): string | null;
464
+ /**
465
+ * Id del registro de analytics abierto: el `sessionId` que devolvió `POST /sdk/feedback`,
466
+ * que la API guarda como `sdkSessionId` en el Feedback en que se convierte la sesión. No es
467
+ * `getSessionId()`. Null hasta que el backend acepta el primer lote y de nuevo al cerrar la
468
+ * sesión: para saber también qué registro se cerró, usa `init({ onFeedbackSession })`.
469
+ */
470
+ getFeedbackSessionId(): string | null;
471
+ /** Avisa al host del registro abierto o cerrado; un fallo del host no corta el envío. */
472
+ private notifyFeedbackSession;
438
473
  /** Activa/desactiva el tracking (identidad + sesión + analytics). Kill-switch del contrato §7bis. */
439
474
  setTrackingEnabled(enabled: boolean): void;
440
475
  /** Registra un evento de analítica (modelo GA: nombre + params). No-op si tracking off. */
@@ -122,6 +122,19 @@ interface DeepdotsLogger {
122
122
  error?: (...args: unknown[]) => void;
123
123
  info?: (...args: unknown[]) => void;
124
124
  }
125
+ /**
126
+ * A session of the analytics channel as the Deepdots API sees it. Every session becomes ONE
127
+ * feedback, and `sessionId` is what the API stores as `sdkSessionId` on it: the value to look
128
+ * that feedback up with (`GET /feedbacks?filter={"where":{"sdkSessionId":"<sessionId>"}}`).
129
+ * A session that closes again keeps that one feedback: the API updates it.
130
+ * - `open`: the API acknowledged the session's first batch; its events keep accumulating.
131
+ * - `closed`: the API acknowledged the session's closing batch (`completed: true`); the
132
+ * feedback is created from it within seconds.
133
+ */
134
+ interface FeedbackSession {
135
+ sessionId: string;
136
+ status: 'open' | 'closed';
137
+ }
125
138
  interface DeepdotsInitParams {
126
139
  /** API key for authentication */
127
140
  apiKey?: string;
@@ -157,6 +170,17 @@ interface DeepdotsInitParams {
157
170
  * logged when `debug` is true.
158
171
  */
159
172
  analytics?: AnalyticsKeys;
173
+ /**
174
+ * Called when the analytics channel opens or closes a session (needs `analytics`), with the
175
+ * id the Deepdots API stores as `sdkSessionId` on the feedback the session becomes: send it
176
+ * to your backend to attach data to that feedback later. A session whose only batch is the
177
+ * closing one reports just `closed`. `closed` does not come when the page unloads before the
178
+ * closing request returns, for a session dropped after 30 min hidden, nor when the closing
179
+ * request fails (the API closes those itself, later): their id already came with `open`. Key
180
+ * by `sessionId`, not by order: the next session's `open` can arrive before the previous
181
+ * one's `closed`. Same value as `getFeedbackSessionId()` while the session is open.
182
+ */
183
+ onFeedbackSession?: (session: FeedbackSession) => void;
160
184
  /**
161
185
  * IP geolocation (country/city) added to analytics. Default `true`. The lookup calls
162
186
  * third-party services (ipapi.co, then ipwho.is, then ipinfo.io as fallbacks, 3 s timeout
@@ -392,6 +416,8 @@ declare class DeepdotsPopups {
392
416
  private analytics;
393
417
  /** feedbackSessionId cacheado del canal de analytics (devuelto por POST /sdk/feedback). */
394
418
  private analyticsFeedbackSessionId;
419
+ /** `init({ onFeedbackSession })`: avisa al host al abrir y cerrar cada registro. */
420
+ private onFeedbackSession;
395
421
  /** Timer del flush periódico de analytics (cada ANALYTICS_FLUSH_INTERVAL_MS). */
396
422
  private analyticsFlushTimer;
397
423
  /** Crash & error reporting (#14–17). Null hasta init(). */
@@ -435,6 +461,15 @@ declare class DeepdotsPopups {
435
461
  getUserId(): string | null;
436
462
  /** Session id de navegación actual. Null si tracking off. */
437
463
  getSessionId(): string | null;
464
+ /**
465
+ * Id del registro de analytics abierto: el `sessionId` que devolvió `POST /sdk/feedback`,
466
+ * que la API guarda como `sdkSessionId` en el Feedback en que se convierte la sesión. No es
467
+ * `getSessionId()`. Null hasta que el backend acepta el primer lote y de nuevo al cerrar la
468
+ * sesión: para saber también qué registro se cerró, usa `init({ onFeedbackSession })`.
469
+ */
470
+ getFeedbackSessionId(): string | null;
471
+ /** Avisa al host del registro abierto o cerrado; un fallo del host no corta el envío. */
472
+ private notifyFeedbackSession;
438
473
  /** Activa/desactiva el tracking (identidad + sesión + analytics). Kill-switch del contrato §7bis. */
439
474
  setTrackingEnabled(enabled: boolean): void;
440
475
  /** Registra un evento de analítica (modelo GA: nombre + params). No-op si tracking off. */
@@ -563,7 +563,9 @@ var init_style = __esm({
563
563
  * repintar\xEDa su web.
564
564
  * 4. El grupo .magicfeedback-radio se cualifica como div.magicfeedback-radio (la clase acaba
565
565
  * tambi\xE9n en el <input> del boolean).
566
- * 5. Al final, un bloque de reglas propias del popup (fuente en los controles y padding del
566
+ * 5. Todo :hover va dentro de @media (hover: hover): en t\xE1ctil WebKit deja el hover pegado
567
+ * en el \xFAltimo punto tocado y la opci\xF3n de la p\xE1gina siguiente parec\xEDa seleccionada.
568
+ * 6. Al final, un bloque de reglas propias del popup (fuente en los controles y padding del
567
569
  * radio nativo).
568
570
  *
569
571
  * Las variables --mf-* se dejan en :root a prop\xF3sito: el modal del priority-list se portea a
@@ -801,9 +803,11 @@ var init_style = __esm({
801
803
  text-underline-offset: 2px;
802
804
  }
803
805
 
804
- .magicfeedback-info-message a:hover,
805
- .magicfeedback-start-message a:hover {
806
- color: var(--mf-primary-hover);
806
+ @media (hover: hover) {
807
+ .magicfeedback-info-message a:hover,
808
+ .magicfeedback-start-message a:hover {
809
+ color: var(--mf-primary-hover);
810
+ }
807
811
  }
808
812
 
809
813
  .magicfeedback-info-message ul,
@@ -1029,10 +1033,12 @@ div.magicfeedback-radio {
1029
1033
  transition: var(--mf-transition-fast);
1030
1034
  }
1031
1035
 
1032
- .magicfeedback-checkbox-container:hover,
1033
- .magicfeedback-radio-container:hover {
1034
- background-color: var(--mf-bg-hover);
1035
- border-color: var(--mf-primary);
1036
+ @media (hover: hover) {
1037
+ .magicfeedback-checkbox-container:hover,
1038
+ .magicfeedback-radio-container:hover {
1039
+ background-color: var(--mf-bg-hover);
1040
+ border-color: var(--mf-primary);
1041
+ }
1036
1042
  }
1037
1043
 
1038
1044
  .magicfeedback-checkbox-container:has(input:checked),
@@ -1099,9 +1105,11 @@ div.magicfeedback-radio {
1099
1105
  height: inherit;
1100
1106
  }
1101
1107
 
1102
- .magicfeedback-boolean-option label:hover {
1103
- background-color: var(--mf-bg-hover);
1104
- border-color: var(--mf-primary);
1108
+ @media (hover: hover) {
1109
+ .magicfeedback-boolean-option label:hover {
1110
+ background-color: var(--mf-bg-hover);
1111
+ border-color: var(--mf-primary);
1112
+ }
1105
1113
  }
1106
1114
 
1107
1115
  .magicfeedback-boolean-option input[type="radio"]:checked + label {
@@ -1172,9 +1180,11 @@ div.magicfeedback-radio {
1172
1180
  border: 2px solid transparent;
1173
1181
  }
1174
1182
 
1175
- .magicfeedback-rating-option-label-container img:hover {
1176
- transform: scale(1.1);
1177
- border-color: var(--mf-primary);
1183
+ @media (hover: hover) {
1184
+ .magicfeedback-rating-option-label-container img:hover {
1185
+ transform: scale(1.1);
1186
+ border-color: var(--mf-primary);
1187
+ }
1178
1188
  }
1179
1189
 
1180
1190
  .magicfeedback-rating-option-label-container input[type="radio"]:checked + img {
@@ -1326,9 +1336,11 @@ div.magicfeedback-radio {
1326
1336
  font-size: 0.8rem;
1327
1337
  }
1328
1338
 
1329
- .magicfeedback-rating-number-option-row .magicfeedback-rating-number-option-label-container:hover .magicfeedback-rating-number-value,
1330
- .magicfeedback-rating-number-option-row-reverse .magicfeedback-rating-number-option-label-container:hover .magicfeedback-rating-number-value {
1331
- background: var(--mf-primary-light);
1339
+ @media (hover: hover) {
1340
+ .magicfeedback-rating-number-option-row .magicfeedback-rating-number-option-label-container:hover .magicfeedback-rating-number-value,
1341
+ .magicfeedback-rating-number-option-row-reverse .magicfeedback-rating-number-option-label-container:hover .magicfeedback-rating-number-value {
1342
+ background: var(--mf-primary-light);
1343
+ }
1332
1344
  }
1333
1345
 
1334
1346
  .magicfeedback-rating-number-option-row .magicfeedback-rating-number-option-label-container input[type="radio"]:checked + .magicfeedback-rating-number-value,
@@ -1365,8 +1377,10 @@ div.magicfeedback-radio {
1365
1377
  font-size: var(--mf-font-size-sm);
1366
1378
  }
1367
1379
 
1368
- .magicfeedback-rating-number-option--extra .magicfeedback-rating-number-option-label-container:hover .magicfeedback-rating-number-value {
1369
- background: var(--mf-primary-light);
1380
+ @media (hover: hover) {
1381
+ .magicfeedback-rating-number-option--extra .magicfeedback-rating-number-option-label-container:hover .magicfeedback-rating-number-value {
1382
+ background: var(--mf-primary-light);
1383
+ }
1370
1384
  }
1371
1385
 
1372
1386
  .magicfeedback-rating-number-option--extra .magicfeedback-rating-number-option-label-container input[type="radio"]:checked + .magicfeedback-rating-number-value {
@@ -1399,9 +1413,11 @@ div.magicfeedback-radio {
1399
1413
  color: var(--mf-primary);
1400
1414
  }
1401
1415
 
1402
- .magicfeedback-rating-number-option-column .magicfeedback-rating-number-option-label-container:hover .magicfeedback-rating-number-value,
1403
- .magicfeedback-rating-number-option-column-reverse .magicfeedback-rating-number-option-label-container:hover .magicfeedback-rating-number-value {
1404
- background: var(--mf-primary-light);
1416
+ @media (hover: hover) {
1417
+ .magicfeedback-rating-number-option-column .magicfeedback-rating-number-option-label-container:hover .magicfeedback-rating-number-value,
1418
+ .magicfeedback-rating-number-option-column-reverse .magicfeedback-rating-number-option-label-container:hover .magicfeedback-rating-number-value {
1419
+ background: var(--mf-primary-light);
1420
+ }
1405
1421
  }
1406
1422
 
1407
1423
  .magicfeedback-rating-number-option-column .magicfeedback-rating-number-option-label-container input[type="radio"]:checked + .magicfeedback-rating-number-value,
@@ -1455,10 +1471,12 @@ div.magicfeedback-radio {
1455
1471
  transition: var(--mf-transition-fast);
1456
1472
  }
1457
1473
 
1458
- .magicfeedback-rating-star-option:not(:checked) > .rating__star:hover,
1459
- .magicfeedback-rating-star-option:not(:checked) > .rating__star:hover ~ span {
1460
- color: #fbbf24;
1461
- transform: scale(1.1);
1474
+ @media (hover: hover) {
1475
+ .magicfeedback-rating-star-option:not(:checked) > .rating__star:hover,
1476
+ .magicfeedback-rating-star-option:not(:checked) > .rating__star:hover ~ span {
1477
+ color: #fbbf24;
1478
+ transform: scale(1.1);
1479
+ }
1462
1480
  }
1463
1481
 
1464
1482
  .magicfeedback-rating-star-selected {
@@ -1499,9 +1517,11 @@ div.magicfeedback-radio {
1499
1517
  transition: var(--mf-transition-fast);
1500
1518
  }
1501
1519
 
1502
- .magicfeedback-image-option-label-container:hover img {
1503
- border-color: var(--mf-primary);
1504
- box-shadow: var(--mf-shadow-md);
1520
+ @media (hover: hover) {
1521
+ .magicfeedback-image-option-label-container:hover img {
1522
+ border-color: var(--mf-primary);
1523
+ box-shadow: var(--mf-shadow-md);
1524
+ }
1505
1525
  }
1506
1526
 
1507
1527
  .magicfeedback-image-option-label-container input:checked + img {
@@ -1542,9 +1562,11 @@ div.magicfeedback-radio {
1542
1562
  background-color: var(--mf-bg-secondary);
1543
1563
  }
1544
1564
 
1545
- .magicfeedback-multi-question-matrix-row-tr:hover {
1546
- background-color: var(--mf-bg-hover);
1547
- box-shadow: var(--mf-shadow-sm);
1565
+ @media (hover: hover) {
1566
+ .magicfeedback-multi-question-matrix-row-tr:hover {
1567
+ background-color: var(--mf-bg-hover);
1568
+ box-shadow: var(--mf-shadow-sm);
1569
+ }
1548
1570
  }
1549
1571
 
1550
1572
  .magicfeedback-multi-question-matrix-table td {
@@ -1580,9 +1602,11 @@ div.magicfeedback-radio {
1580
1602
  transition: var(--mf-transition-fast);
1581
1603
  }
1582
1604
 
1583
- .magicfeedback-priority-list-item:hover {
1584
- box-shadow: var(--mf-shadow-sm);
1585
- border-color: var(--mf-primary);
1605
+ @media (hover: hover) {
1606
+ .magicfeedback-priority-list-item:hover {
1607
+ box-shadow: var(--mf-shadow-sm);
1608
+ border-color: var(--mf-primary);
1609
+ }
1586
1610
  }
1587
1611
 
1588
1612
  .magicfeedback-priority-list-item-label {
@@ -1603,10 +1627,12 @@ div.magicfeedback-radio {
1603
1627
  user-select: none;
1604
1628
  }
1605
1629
 
1606
- .magicfeedback-priority-list-arrow-up:hover,
1607
- .magicfeedback-priority-list-arrow-down:hover {
1608
- color: var(--mf-primary);
1609
- transform: scale(1.2);
1630
+ @media (hover: hover) {
1631
+ .magicfeedback-priority-list-arrow-up:hover,
1632
+ .magicfeedback-priority-list-arrow-down:hover {
1633
+ color: var(--mf-primary);
1634
+ transform: scale(1.2);
1635
+ }
1610
1636
  }
1611
1637
 
1612
1638
  /* ========================================
@@ -1713,10 +1739,12 @@ div.magicfeedback-radio {
1713
1739
  box-shadow: var(--mf-shadow-md);
1714
1740
  }
1715
1741
 
1716
- .magicfeedback-submit:hover {
1717
- background-color: var(--mf-primary-hover);
1718
- box-shadow: var(--mf-shadow-lg);
1719
- transform: translateY(-2px);
1742
+ @media (hover: hover) {
1743
+ .magicfeedback-submit:hover {
1744
+ background-color: var(--mf-primary-hover);
1745
+ box-shadow: var(--mf-shadow-lg);
1746
+ transform: translateY(-2px);
1747
+ }
1720
1748
  }
1721
1749
 
1722
1750
  .magicfeedback-submit:active {
@@ -1742,9 +1770,11 @@ div.magicfeedback-radio {
1742
1770
  transition: var(--mf-transition-fast);
1743
1771
  }
1744
1772
 
1745
- .magicfeedback-back:hover {
1746
- border-color: var(--mf-primary);
1747
- background-color: var(--mf-bg-hover);
1773
+ @media (hover: hover) {
1774
+ .magicfeedback-back:hover {
1775
+ border-color: var(--mf-primary);
1776
+ background-color: var(--mf-bg-hover);
1777
+ }
1748
1778
  }
1749
1779
 
1750
1780
  /* ========================================
@@ -1764,8 +1794,10 @@ div.magicfeedback-radio {
1764
1794
  transition: var(--mf-transition-fast);
1765
1795
  }
1766
1796
 
1767
- .magicfeedback-skip-container:hover {
1768
- color: var(--mf-primary);
1797
+ @media (hover: hover) {
1798
+ .magicfeedback-skip-container:hover {
1799
+ color: var(--mf-primary);
1800
+ }
1769
1801
  }
1770
1802
 
1771
1803
  .magicfeedback-skip-container input[type="checkbox"] {
@@ -1906,9 +1938,11 @@ div.magicfeedback-radio {
1906
1938
  box-shadow: var(--mf-shadow-sm);
1907
1939
  }
1908
1940
 
1909
- .magicfeedback-button:hover {
1910
- background: var(--mf-bg-hover);
1911
- border-color: var(--mf-primary);
1941
+ @media (hover: hover) {
1942
+ .magicfeedback-button:hover {
1943
+ background: var(--mf-bg-hover);
1944
+ border-color: var(--mf-primary);
1945
+ }
1912
1946
  }
1913
1947
 
1914
1948
  .magicfeedback-button:active {
@@ -1922,8 +1956,10 @@ div.magicfeedback-radio {
1922
1956
  box-shadow: var(--mf-shadow-md);
1923
1957
  }
1924
1958
 
1925
- .magicfeedback-button-primary:hover {
1926
- background: var(--mf-primary-hover);
1959
+ @media (hover: hover) {
1960
+ .magicfeedback-button-primary:hover {
1961
+ background: var(--mf-primary-hover);
1962
+ }
1927
1963
  }
1928
1964
 
1929
1965
  .magicfeedback-button-primary:active {
@@ -1949,8 +1985,10 @@ div.magicfeedback-radio {
1949
1985
  box-shadow: var(--mf-shadow-md);
1950
1986
  }
1951
1987
 
1952
- .magicfeedback-start-message-button:hover {
1953
- background: var(--mf-primary-hover);
1988
+ @media (hover: hover) {
1989
+ .magicfeedback-start-message-button:hover {
1990
+ background: var(--mf-primary-hover);
1991
+ }
1954
1992
  }
1955
1993
 
1956
1994
  /* ========================================
@@ -2086,12 +2124,18 @@ div.magicfeedback-radio {
2086
2124
  color: #666;
2087
2125
  }
2088
2126
 
2089
- .magicfeedback-modal-close:hover,
2090
2127
  .magicfeedback-modal-close:focus {
2091
2128
  color: #000;
2092
2129
  outline: none;
2093
2130
  }
2094
2131
 
2132
+ @media (hover: hover) {
2133
+ .magicfeedback-modal-close:hover {
2134
+ color: #000;
2135
+ outline: none;
2136
+ }
2137
+ }
2138
+
2095
2139
  /* ========================================
2096
2140
  FILE / IMAGE UPLOAD
2097
2141
  ======================================== */
@@ -2135,9 +2179,11 @@ div.magicfeedback-radio {
2135
2179
  transition: var(--mf-transition);
2136
2180
  }
2137
2181
 
2138
- .magicfeedback-upload-dropzone:hover {
2139
- border-color: var(--mf-primary);
2140
- background-color: var(--mf-bg-hover);
2182
+ @media (hover: hover) {
2183
+ .magicfeedback-upload-dropzone:hover {
2184
+ border-color: var(--mf-primary);
2185
+ background-color: var(--mf-bg-hover);
2186
+ }
2141
2187
  }
2142
2188
 
2143
2189
  .magicfeedback-upload-dropzone:focus-within {
@@ -2275,13 +2321,20 @@ div.magicfeedback-radio {
2275
2321
  transition: var(--mf-transition-fast);
2276
2322
  }
2277
2323
 
2278
- .magicfeedback-upload-remove:hover,
2279
2324
  .magicfeedback-upload-remove:focus-visible {
2280
2325
  color: var(--mf-error);
2281
2326
  background-color: var(--mf-primary-light);
2282
2327
  outline: none;
2283
2328
  }
2284
2329
 
2330
+ @media (hover: hover) {
2331
+ .magicfeedback-upload-remove:hover {
2332
+ color: var(--mf-error);
2333
+ background-color: var(--mf-primary-light);
2334
+ outline: none;
2335
+ }
2336
+ }
2337
+
2285
2338
  .magicfeedback-upload-remove svg {
2286
2339
  width: 16px;
2287
2340
  height: 16px;
@@ -4376,8 +4429,10 @@ async function safeBodyText(res) {
4376
4429
  function createFeedbackSink(options) {
4377
4430
  let feedbackSessionId;
4378
4431
  let firstPostInFlight = null;
4432
+ let generation = 0;
4379
4433
  const url = `${options.baseUrl}/sdk/feedback`;
4380
4434
  const post = async (body, final, sessionEnd = false) => {
4435
+ const postGeneration = generation;
4381
4436
  const json = JSON.stringify(body);
4382
4437
  const small = json.length <= KEEPALIVE_MAX_BYTES;
4383
4438
  if (final) options.log?.("[DeepdotsAnalytics] final flush via fetch keepalive");
@@ -4407,9 +4462,25 @@ function createFeedbackSink(options) {
4407
4462
  );
4408
4463
  return;
4409
4464
  }
4410
- if (sessionEnd) return;
4465
+ if (sessionEnd) {
4466
+ let closedId = body.sessionId;
4467
+ try {
4468
+ const data = await res.json();
4469
+ closedId = data?.sessionId ?? closedId;
4470
+ } catch {
4471
+ }
4472
+ try {
4473
+ if (closedId) options.onSessionClosed?.(closedId);
4474
+ } catch {
4475
+ }
4476
+ return;
4477
+ }
4411
4478
  try {
4412
4479
  const data = await res.json();
4480
+ if (postGeneration !== generation) {
4481
+ options.log?.("[DeepdotsAnalytics] sessionId of a closed session ignored:", data?.sessionId);
4482
+ return;
4483
+ }
4413
4484
  if (data?.sessionId && data.sessionId !== feedbackSessionId) {
4414
4485
  feedbackSessionId = data.sessionId;
4415
4486
  options.log?.("[DeepdotsAnalytics] feedbackSessionId cached:", feedbackSessionId);
@@ -4422,6 +4493,7 @@ function createFeedbackSink(options) {
4422
4493
  if (meta?.resetSession) {
4423
4494
  feedbackSessionId = void 0;
4424
4495
  firstPostInFlight = null;
4496
+ generation++;
4425
4497
  options.onSessionReset?.();
4426
4498
  return;
4427
4499
  }
@@ -4436,6 +4508,7 @@ function createFeedbackSink(options) {
4436
4508
  if (sessionEnd) {
4437
4509
  feedbackSessionId = void 0;
4438
4510
  firstPostInFlight = null;
4511
+ generation++;
4439
4512
  options.onSessionReset?.();
4440
4513
  } else if (!feedbackSessionId) {
4441
4514
  firstPostInFlight = sent.then(
@@ -5086,6 +5159,8 @@ var DeepdotsPopups = class {
5086
5159
  this.analytics = null;
5087
5160
  /** feedbackSessionId cacheado del canal de analytics (devuelto por POST /sdk/feedback). */
5088
5161
  this.analyticsFeedbackSessionId = void 0;
5162
+ /** `init({ onFeedbackSession })`: avisa al host al abrir y cerrar cada registro. */
5163
+ this.onFeedbackSession = void 0;
5089
5164
  /** Timer del flush periódico de analytics (cada ANALYTICS_FLUSH_INTERVAL_MS). */
5090
5165
  this.analyticsFlushTimer = void 0;
5091
5166
  /** Crash & error reporting (#14–17). Null hasta init(). */
@@ -5143,6 +5218,7 @@ var DeepdotsPopups = class {
5143
5218
  this.renderChrome = config.renderChrome ?? true;
5144
5219
  this.showProgressBar = config.showProgressBar;
5145
5220
  this.surveyCss = config.surveyCss;
5221
+ this.onFeedbackSession = config.onFeedbackSession;
5146
5222
  this.config = {
5147
5223
  apiKey: config.apiKey || void 0,
5148
5224
  debug: config.debug || false,
@@ -5176,7 +5252,9 @@ var DeepdotsPopups = class {
5176
5252
  log: (...a) => this.log(...a),
5177
5253
  onSessionId: (id) => {
5178
5254
  this.analyticsFeedbackSessionId = id;
5255
+ this.notifyFeedbackSession(id, "open");
5179
5256
  },
5257
+ onSessionClosed: (id) => this.notifyFeedbackSession(id, "closed"),
5180
5258
  onSessionReset: () => {
5181
5259
  this.analyticsFeedbackSessionId = void 0;
5182
5260
  }
@@ -5251,6 +5329,23 @@ var DeepdotsPopups = class {
5251
5329
  getSessionId() {
5252
5330
  return this.tracking?.getSessionId() ?? null;
5253
5331
  }
5332
+ /**
5333
+ * Id del registro de analytics abierto: el `sessionId` que devolvió `POST /sdk/feedback`,
5334
+ * que la API guarda como `sdkSessionId` en el Feedback en que se convierte la sesión. No es
5335
+ * `getSessionId()`. Null hasta que el backend acepta el primer lote y de nuevo al cerrar la
5336
+ * sesión: para saber también qué registro se cerró, usa `init({ onFeedbackSession })`.
5337
+ */
5338
+ getFeedbackSessionId() {
5339
+ return this.analyticsFeedbackSessionId ?? null;
5340
+ }
5341
+ /** Avisa al host del registro abierto o cerrado; un fallo del host no corta el envío. */
5342
+ notifyFeedbackSession(sessionId, status) {
5343
+ try {
5344
+ this.onFeedbackSession?.({ sessionId, status });
5345
+ } catch (error) {
5346
+ this.log("analytics \xB7 onFeedbackSession threw:", error);
5347
+ }
5348
+ }
5254
5349
  /** Activa/desactiva el tracking (identidad + sesión + analytics). Kill-switch del contrato §7bis. */
5255
5350
  setTrackingEnabled(enabled) {
5256
5351
  if (!enabled) this.closeSession("tracking_disabled");