@grainql/analytics-web 2.6.0 → 2.7.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.
Files changed (79) hide show
  1. package/dist/attention-quality.d.ts +128 -0
  2. package/dist/attention-quality.d.ts.map +1 -0
  3. package/dist/attention-quality.js +246 -0
  4. package/dist/cjs/attention-quality.d.ts +128 -0
  5. package/dist/cjs/attention-quality.d.ts.map +1 -0
  6. package/dist/cjs/attention-quality.js +246 -0
  7. package/dist/cjs/attention-quality.js.map +1 -0
  8. package/dist/cjs/heatmap-tracking.d.ts +3 -0
  9. package/dist/cjs/heatmap-tracking.d.ts.map +1 -1
  10. package/dist/cjs/heatmap-tracking.js +36 -1
  11. package/dist/cjs/heatmap-tracking.js.map +1 -1
  12. package/dist/cjs/index.d.ts +5 -0
  13. package/dist/cjs/index.d.ts.map +1 -1
  14. package/dist/cjs/index.js.map +1 -1
  15. package/dist/cjs/interaction-tracking.d.ts.map +1 -1
  16. package/dist/cjs/interaction-tracking.js +2 -1
  17. package/dist/cjs/interaction-tracking.js.map +1 -1
  18. package/dist/cjs/section-tracking.d.ts +3 -0
  19. package/dist/cjs/section-tracking.d.ts.map +1 -1
  20. package/dist/cjs/section-tracking.js +29 -0
  21. package/dist/cjs/section-tracking.js.map +1 -1
  22. package/dist/cjs/text-utils.d.ts +14 -0
  23. package/dist/cjs/text-utils.d.ts.map +1 -0
  24. package/dist/cjs/text-utils.js +49 -0
  25. package/dist/cjs/text-utils.js.map +1 -0
  26. package/dist/cjs/types/auto-tracking.d.ts +3 -0
  27. package/dist/cjs/types/auto-tracking.d.ts.map +1 -1
  28. package/dist/cjs/types/heatmap-tracking.d.ts +3 -0
  29. package/dist/cjs/types/heatmap-tracking.d.ts.map +1 -1
  30. package/dist/esm/attention-quality.d.ts +128 -0
  31. package/dist/esm/attention-quality.d.ts.map +1 -0
  32. package/dist/esm/attention-quality.js +242 -0
  33. package/dist/esm/attention-quality.js.map +1 -0
  34. package/dist/esm/heatmap-tracking.d.ts +3 -0
  35. package/dist/esm/heatmap-tracking.d.ts.map +1 -1
  36. package/dist/esm/heatmap-tracking.js +36 -1
  37. package/dist/esm/heatmap-tracking.js.map +1 -1
  38. package/dist/esm/index.d.ts +5 -0
  39. package/dist/esm/index.d.ts.map +1 -1
  40. package/dist/esm/index.js.map +1 -1
  41. package/dist/esm/interaction-tracking.d.ts.map +1 -1
  42. package/dist/esm/interaction-tracking.js +2 -1
  43. package/dist/esm/interaction-tracking.js.map +1 -1
  44. package/dist/esm/section-tracking.d.ts +3 -0
  45. package/dist/esm/section-tracking.d.ts.map +1 -1
  46. package/dist/esm/section-tracking.js +29 -0
  47. package/dist/esm/section-tracking.js.map +1 -1
  48. package/dist/esm/text-utils.d.ts +14 -0
  49. package/dist/esm/text-utils.d.ts.map +1 -0
  50. package/dist/esm/text-utils.js +45 -0
  51. package/dist/esm/text-utils.js.map +1 -0
  52. package/dist/esm/types/auto-tracking.d.ts +3 -0
  53. package/dist/esm/types/auto-tracking.d.ts.map +1 -1
  54. package/dist/esm/types/heatmap-tracking.d.ts +3 -0
  55. package/dist/esm/types/heatmap-tracking.d.ts.map +1 -1
  56. package/dist/heatmap-tracking.d.ts +3 -0
  57. package/dist/heatmap-tracking.d.ts.map +1 -1
  58. package/dist/heatmap-tracking.js +36 -1
  59. package/dist/index.d.ts +5 -0
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.global.dev.js +330 -11
  62. package/dist/index.global.dev.js.map +4 -4
  63. package/dist/index.global.js +2 -2
  64. package/dist/index.global.js.map +4 -4
  65. package/dist/index.js +9 -0
  66. package/dist/index.mjs +9 -0
  67. package/dist/interaction-tracking.d.ts.map +1 -1
  68. package/dist/interaction-tracking.js +2 -1
  69. package/dist/section-tracking.d.ts +3 -0
  70. package/dist/section-tracking.d.ts.map +1 -1
  71. package/dist/section-tracking.js +29 -0
  72. package/dist/text-utils.d.ts +14 -0
  73. package/dist/text-utils.d.ts.map +1 -0
  74. package/dist/text-utils.js +49 -0
  75. package/dist/types/auto-tracking.d.ts +3 -0
  76. package/dist/types/auto-tracking.d.ts.map +1 -1
  77. package/dist/types/heatmap-tracking.d.ts +3 -0
  78. package/dist/types/heatmap-tracking.d.ts.map +1 -1
  79. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- /* Grain Analytics Web SDK v2.6.0 | MIT License | Development Build */
1
+ /* Grain Analytics Web SDK v2.7.1 | MIT License | Development Build */
2
2
  "use strict";
3
3
  var Grain = (() => {
4
4
  var __defProp = Object.defineProperty;
@@ -22,23 +22,271 @@ var Grain = (() => {
22
22
  };
23
23
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
24
24
 
25
+ // src/attention-quality.ts
26
+ var DEFAULT_OPTIONS, AttentionQualityManager;
27
+ var init_attention_quality = __esm({
28
+ "src/attention-quality.ts"() {
29
+ "use strict";
30
+ DEFAULT_OPTIONS = {
31
+ maxSectionDuration: 9e3,
32
+ // 9 seconds
33
+ minScrollDistance: 100,
34
+ // 100 pixels
35
+ idleThreshold: 3e4
36
+ // 30 seconds
37
+ };
38
+ AttentionQualityManager = class {
39
+ constructor(activityDetector, options = {}) {
40
+ this.isDestroyed = false;
41
+ // Page visibility tracking
42
+ this.isPageVisible = true;
43
+ this.visibilityChangeHandler = null;
44
+ // Section attention state
45
+ this.sectionStates = /* @__PURE__ */ new Map();
46
+ // Policies applied reasons (for debugging/traceability)
47
+ this.lastFilterReason = null;
48
+ this.activityDetector = activityDetector;
49
+ this.options = {
50
+ ...DEFAULT_OPTIONS,
51
+ ...options,
52
+ debug: options.debug ?? false
53
+ };
54
+ this.setupPageVisibilityTracking();
55
+ }
56
+ /**
57
+ * Setup page visibility tracking
58
+ */
59
+ setupPageVisibilityTracking() {
60
+ if (typeof document === "undefined")
61
+ return;
62
+ this.isPageVisible = document.visibilityState === "visible";
63
+ this.visibilityChangeHandler = () => {
64
+ const wasVisible = this.isPageVisible;
65
+ this.isPageVisible = document.visibilityState === "visible";
66
+ if (!this.isPageVisible && wasVisible) {
67
+ this.log("Page hidden - tracking paused");
68
+ } else if (this.isPageVisible && !wasVisible) {
69
+ this.log("Page visible - tracking resumed");
70
+ this.resetAllSections();
71
+ }
72
+ };
73
+ document.addEventListener("visibilitychange", this.visibilityChangeHandler);
74
+ }
75
+ /**
76
+ * Check if tracking should be allowed (global check)
77
+ * Returns true if tracking is allowed, false if it should be paused
78
+ */
79
+ shouldTrack() {
80
+ if (!this.isPageVisible) {
81
+ this.lastFilterReason = "page_hidden";
82
+ return false;
83
+ }
84
+ if (!this.activityDetector.isActive(this.options.idleThreshold)) {
85
+ this.lastFilterReason = "user_idle";
86
+ return false;
87
+ }
88
+ this.lastFilterReason = null;
89
+ return true;
90
+ }
91
+ /**
92
+ * Check if section view tracking should be allowed for a specific section
93
+ * @param sectionName - Section identifier
94
+ * @param currentScrollY - Current scroll position
95
+ * @returns Object with shouldTrack boolean and optional reason
96
+ */
97
+ shouldTrackSection(sectionName, currentScrollY) {
98
+ if (!this.shouldTrack()) {
99
+ return {
100
+ shouldTrack: false,
101
+ reason: this.lastFilterReason || "global_policy"
102
+ };
103
+ }
104
+ let state = this.sectionStates.get(sectionName);
105
+ if (!state) {
106
+ state = {
107
+ sectionName,
108
+ currentDuration: 0,
109
+ lastScrollPosition: currentScrollY,
110
+ lastResetTime: Date.now()
111
+ };
112
+ this.sectionStates.set(sectionName, state);
113
+ }
114
+ const scrollDistance = Math.abs(currentScrollY - state.lastScrollPosition);
115
+ const hasScrolledEnough = scrollDistance >= this.options.minScrollDistance;
116
+ if (hasScrolledEnough) {
117
+ this.log(`Section "${sectionName}": Attention reset due to ${Math.round(scrollDistance)}px scroll`);
118
+ state.currentDuration = 0;
119
+ state.lastScrollPosition = currentScrollY;
120
+ state.lastResetTime = Date.now();
121
+ return {
122
+ shouldTrack: true,
123
+ resetAttention: true
124
+ };
125
+ }
126
+ if (state.currentDuration >= this.options.maxSectionDuration) {
127
+ return {
128
+ shouldTrack: false,
129
+ reason: "max_duration_reached"
130
+ };
131
+ }
132
+ return {
133
+ shouldTrack: true
134
+ };
135
+ }
136
+ /**
137
+ * Update section duration (call this when tracking a section view event)
138
+ * @param sectionName - Section identifier
139
+ * @param durationMs - Duration to add to current attention block
140
+ */
141
+ updateSectionDuration(sectionName, durationMs) {
142
+ const state = this.sectionStates.get(sectionName);
143
+ if (state) {
144
+ state.currentDuration += durationMs;
145
+ if (state.currentDuration >= this.options.maxSectionDuration) {
146
+ this.log(`Section "${sectionName}": Max duration cap reached (${state.currentDuration}ms)`);
147
+ }
148
+ }
149
+ }
150
+ /**
151
+ * Reset attention for a specific section (call when user navigates to different section)
152
+ * @param sectionName - Section identifier
153
+ */
154
+ resetSection(sectionName) {
155
+ const state = this.sectionStates.get(sectionName);
156
+ if (state) {
157
+ this.log(`Section "${sectionName}": Attention reset (section exit)`);
158
+ state.currentDuration = 0;
159
+ state.lastResetTime = Date.now();
160
+ }
161
+ }
162
+ /**
163
+ * Reset all section attention states
164
+ */
165
+ resetAllSections() {
166
+ this.log("Resetting all section attention states");
167
+ for (const state of this.sectionStates.values()) {
168
+ state.currentDuration = 0;
169
+ state.lastResetTime = Date.now();
170
+ }
171
+ }
172
+ /**
173
+ * Get current attention state for a section (for debugging/monitoring)
174
+ */
175
+ getSectionState(sectionName) {
176
+ return this.sectionStates.get(sectionName);
177
+ }
178
+ /**
179
+ * Get reason why last tracking attempt was filtered
180
+ */
181
+ getLastFilterReason() {
182
+ return this.lastFilterReason;
183
+ }
184
+ /**
185
+ * Check if scroll tracking should be allowed
186
+ * Similar to shouldTrack() but also checks scroll-specific conditions
187
+ */
188
+ shouldTrackScroll(previousScrollY, currentScrollY) {
189
+ if (!this.shouldTrack()) {
190
+ return {
191
+ shouldTrack: false,
192
+ reason: this.lastFilterReason || "global_policy"
193
+ };
194
+ }
195
+ const scrollDistance = Math.abs(currentScrollY - previousScrollY);
196
+ if (scrollDistance < 10) {
197
+ return {
198
+ shouldTrack: false,
199
+ reason: "scroll_too_small"
200
+ };
201
+ }
202
+ return {
203
+ shouldTrack: true
204
+ };
205
+ }
206
+ /**
207
+ * Get all active policies as object (for monitoring/debugging)
208
+ */
209
+ getPolicies() {
210
+ return {
211
+ maxSectionDuration: this.options.maxSectionDuration,
212
+ minScrollDistance: this.options.minScrollDistance,
213
+ idleThreshold: this.options.idleThreshold
214
+ };
215
+ }
216
+ /**
217
+ * Get current tracking state (for monitoring/debugging)
218
+ */
219
+ getTrackingState() {
220
+ return {
221
+ isPageVisible: this.isPageVisible,
222
+ isUserActive: this.activityDetector.isActive(this.options.idleThreshold),
223
+ timeSinceLastActivity: this.activityDetector.getTimeSinceLastActivity(),
224
+ activeSections: this.sectionStates.size
225
+ };
226
+ }
227
+ /**
228
+ * Log debug messages
229
+ */
230
+ log(...args) {
231
+ if (this.options.debug) {
232
+ console.log("[AttentionQuality]", ...args);
233
+ }
234
+ }
235
+ /**
236
+ * Destroy and cleanup
237
+ */
238
+ destroy() {
239
+ if (this.isDestroyed)
240
+ return;
241
+ this.isDestroyed = true;
242
+ if (this.visibilityChangeHandler && typeof document !== "undefined") {
243
+ document.removeEventListener("visibilitychange", this.visibilityChangeHandler);
244
+ this.visibilityChangeHandler = null;
245
+ }
246
+ this.sectionStates.clear();
247
+ }
248
+ };
249
+ }
250
+ });
251
+
252
+ // src/text-utils.ts
253
+ function removeEmojis(text) {
254
+ if (!text)
255
+ return void 0;
256
+ return text.replace(/[\u{1F300}-\u{1F9FF}]/gu, "").replace(/[\u{1F600}-\u{1F64F}]/gu, "").replace(/[\u{1F680}-\u{1F6FF}]/gu, "").replace(/[\u{2600}-\u{26FF}]/gu, "").replace(/[\u{2700}-\u{27BF}]/gu, "").replace(/[\u{1F900}-\u{1F9FF}]/gu, "").replace(/[\u{1F1E0}-\u{1F1FF}]/gu, "").replace(/[\u{200D}]/gu, "").replace(/[\u{FE0F}]/gu, "").replace(/[\u{20E3}]/gu, "").trim();
257
+ }
258
+ function cleanElementText(text, maxLength = 100) {
259
+ if (!text)
260
+ return void 0;
261
+ const cleaned = removeEmojis(text);
262
+ if (!cleaned)
263
+ return void 0;
264
+ return cleaned.substring(0, maxLength) || void 0;
265
+ }
266
+ var init_text_utils = __esm({
267
+ "src/text-utils.ts"() {
268
+ "use strict";
269
+ }
270
+ });
271
+
25
272
  // src/heatmap-tracking.ts
26
273
  var heatmap_tracking_exports = {};
27
274
  __export(heatmap_tracking_exports, {
28
275
  HeatmapTrackingManager: () => HeatmapTrackingManager
29
276
  });
30
- var DEFAULT_OPTIONS, HeatmapTrackingManager;
277
+ var DEFAULT_OPTIONS2, HeatmapTrackingManager;
31
278
  var init_heatmap_tracking = __esm({
32
279
  "src/heatmap-tracking.ts"() {
33
280
  "use strict";
34
- DEFAULT_OPTIONS = {
281
+ init_attention_quality();
282
+ init_text_utils();
283
+ DEFAULT_OPTIONS2 = {
35
284
  scrollDebounceDelay: 100,
36
285
  batchDelay: 2e3,
37
286
  maxBatchSize: 20,
38
287
  debug: false
39
288
  };
40
289
  HeatmapTrackingManager = class {
41
- // 3 seconds - same as section tracking
42
290
  constructor(tracker, options = {}) {
43
291
  this.isDestroyed = false;
44
292
  // Tracking state
@@ -55,7 +303,19 @@ var Grain = (() => {
55
303
  this.lastScrollTime = Date.now();
56
304
  this.SPLIT_DURATION = 3e3;
57
305
  this.tracker = tracker;
58
- this.options = { ...DEFAULT_OPTIONS, ...options };
306
+ this.options = { ...DEFAULT_OPTIONS2, ...options };
307
+ this.attentionQuality = new AttentionQualityManager(
308
+ tracker.getActivityDetector(),
309
+ {
310
+ maxSectionDuration: 9e3,
311
+ // 9 seconds per viewport section
312
+ minScrollDistance: 100,
313
+ // 100 pixels
314
+ idleThreshold: 3e4,
315
+ // 30 seconds
316
+ debug: this.options.debug
317
+ }
318
+ );
59
319
  if (typeof window !== "undefined" && typeof document !== "undefined") {
60
320
  if (document.readyState === "loading") {
61
321
  document.addEventListener("DOMContentLoaded", () => this.initialize());
@@ -133,12 +393,27 @@ var Grain = (() => {
133
393
  return;
134
394
  if (!this.tracker.hasConsent("analytics"))
135
395
  return;
396
+ if (!this.attentionQuality.shouldTrack()) {
397
+ this.log("Scroll tracking paused:", this.attentionQuality.getLastFilterReason());
398
+ return;
399
+ }
136
400
  const currentTime = Date.now();
137
401
  const duration = currentTime - this.currentScrollState.entryTime;
138
402
  if (duration > 1e3) {
139
403
  const scrollY = window.scrollY || window.pageYOffset;
140
404
  const viewportHeight = window.innerHeight;
141
405
  const pageHeight = document.documentElement.scrollHeight;
406
+ const sectionKey = `viewport_section_${this.currentScrollState.viewportSection}`;
407
+ const attentionCheck = this.attentionQuality.shouldTrackSection(sectionKey, scrollY);
408
+ if (attentionCheck.resetAttention) {
409
+ this.log(`Viewport section ${this.currentScrollState.viewportSection}: Attention reset`);
410
+ this.currentScrollState.entryTime = currentTime;
411
+ return;
412
+ }
413
+ if (!attentionCheck.shouldTrack) {
414
+ this.log(`Viewport section ${this.currentScrollState.viewportSection}: ${attentionCheck.reason}`);
415
+ return;
416
+ }
142
417
  const scrollData = {
143
418
  pageUrl: window.location.href,
144
419
  viewportSection: this.currentScrollState.viewportSection,
@@ -161,6 +436,7 @@ var Grain = (() => {
161
436
  is_split: true
162
437
  // Flag to indicate periodic tracking, not final exit
163
438
  }, { flush: true });
439
+ this.attentionQuality.updateSectionDuration(sectionKey, duration);
164
440
  this.currentScrollState.entryTime = currentTime;
165
441
  }
166
442
  }, this.SPLIT_DURATION);
@@ -210,7 +486,7 @@ var Grain = (() => {
210
486
  const pageX = Math.round(event.pageX);
211
487
  const pageY = Math.round(event.pageY);
212
488
  const elementTag = element.tagName?.toLowerCase() || "unknown";
213
- const elementText = element.textContent?.trim().substring(0, 100);
489
+ const elementText = cleanElementText(element.textContent);
214
490
  const clickData = {
215
491
  pageUrl,
216
492
  xpath,
@@ -276,6 +552,8 @@ var Grain = (() => {
276
552
  };
277
553
  this.pendingScrolls.push(scrollData);
278
554
  }
555
+ const prevSectionKey = `viewport_section_${this.currentScrollState.viewportSection}`;
556
+ this.attentionQuality.resetSection(prevSectionKey);
279
557
  }
280
558
  if (!this.currentScrollState || this.currentScrollState.viewportSection !== viewportSection) {
281
559
  this.currentScrollState = {
@@ -448,6 +726,7 @@ var Grain = (() => {
448
726
  clearInterval(this.periodicScrollTimer);
449
727
  this.periodicScrollTimer = null;
450
728
  }
729
+ this.attentionQuality.destroy();
451
730
  this.flushPendingEvents();
452
731
  }
453
732
  };
@@ -463,6 +742,7 @@ var Grain = (() => {
463
742
  var init_interaction_tracking = __esm({
464
743
  "src/interaction-tracking.ts"() {
465
744
  "use strict";
745
+ init_text_utils();
466
746
  InteractionTrackingManager = class {
467
747
  constructor(tracker, interactions, config = {}) {
468
748
  this.isDestroyed = false;
@@ -541,7 +821,7 @@ var Grain = (() => {
541
821
  interaction_description: interaction.description,
542
822
  interaction_priority: interaction.priority,
543
823
  element_tag: element.tagName?.toLowerCase(),
544
- element_text: element.textContent?.trim().substring(0, 100),
824
+ element_text: cleanElementText(element.textContent),
545
825
  element_id: element.id || void 0,
546
826
  element_class: element.className || void 0,
547
827
  ...isNavigationLink && { href: element.href },
@@ -720,11 +1000,12 @@ var Grain = (() => {
720
1000
  __export(section_tracking_exports, {
721
1001
  SectionTrackingManager: () => SectionTrackingManager
722
1002
  });
723
- var DEFAULT_OPTIONS2, SectionTrackingManager;
1003
+ var DEFAULT_OPTIONS3, SectionTrackingManager;
724
1004
  var init_section_tracking = __esm({
725
1005
  "src/section-tracking.ts"() {
726
1006
  "use strict";
727
- DEFAULT_OPTIONS2 = {
1007
+ init_attention_quality();
1008
+ DEFAULT_OPTIONS3 = {
728
1009
  minDwellTime: 1e3,
729
1010
  // 1 second minimum
730
1011
  scrollVelocityThreshold: 500,
@@ -737,7 +1018,6 @@ var Grain = (() => {
737
1018
  debug: false
738
1019
  };
739
1020
  SectionTrackingManager = class {
740
- // 3 seconds
741
1021
  constructor(tracker, sections, options = {}) {
742
1022
  this.isDestroyed = false;
743
1023
  // Tracking state
@@ -758,7 +1038,19 @@ var Grain = (() => {
758
1038
  this.SPLIT_DURATION = 3e3;
759
1039
  this.tracker = tracker;
760
1040
  this.sections = sections;
761
- this.options = { ...DEFAULT_OPTIONS2, ...options };
1041
+ this.options = { ...DEFAULT_OPTIONS3, ...options };
1042
+ this.attentionQuality = new AttentionQualityManager(
1043
+ tracker.getActivityDetector(),
1044
+ {
1045
+ maxSectionDuration: 9e3,
1046
+ // 9 seconds
1047
+ minScrollDistance: 100,
1048
+ // 100 pixels
1049
+ idleThreshold: 3e4,
1050
+ // 30 seconds
1051
+ debug: this.options.debug
1052
+ }
1053
+ );
762
1054
  if (typeof window !== "undefined" && typeof document !== "undefined") {
763
1055
  if (document.readyState === "loading") {
764
1056
  document.addEventListener("DOMContentLoaded", () => this.initialize());
@@ -892,6 +1184,21 @@ var Grain = (() => {
892
1184
  const now = Date.now();
893
1185
  const duration = now - state.entryTime;
894
1186
  if (duration >= this.options.minDwellTime) {
1187
+ const currentScrollY = window.scrollY;
1188
+ const attentionCheck = this.attentionQuality.shouldTrackSection(
1189
+ state.config.sectionName,
1190
+ currentScrollY
1191
+ );
1192
+ if (attentionCheck.resetAttention) {
1193
+ this.log(`Section "${state.config.sectionName}": Attention reset, restarting timer`);
1194
+ state.entryTime = now;
1195
+ state.entryScrollSpeed = this.scrollVelocity;
1196
+ return;
1197
+ }
1198
+ if (!attentionCheck.shouldTrack) {
1199
+ this.log(`Section "${state.config.sectionName}": Tracking paused - ${attentionCheck.reason}`);
1200
+ return;
1201
+ }
895
1202
  const viewData = {
896
1203
  sectionName: state.config.sectionName,
897
1204
  sectionType: state.config.sectionType,
@@ -922,6 +1229,7 @@ var Grain = (() => {
922
1229
  is_split: true
923
1230
  // Flag to indicate this is a periodic split, not final exit
924
1231
  });
1232
+ this.attentionQuality.updateSectionDuration(state.config.sectionName, duration);
925
1233
  state.entryTime = now;
926
1234
  state.entryScrollSpeed = this.scrollVelocity;
927
1235
  }
@@ -944,6 +1252,7 @@ var Grain = (() => {
944
1252
  */
945
1253
  handleSectionExit(state) {
946
1254
  this.stopPeriodicTracking(state.config.sectionName);
1255
+ this.attentionQuality.resetSection(state.config.sectionName);
947
1256
  if (state.entryTime === null)
948
1257
  return;
949
1258
  state.exitTime = Date.now();
@@ -1140,6 +1449,7 @@ var Grain = (() => {
1140
1449
  this.intersectionObserver.disconnect();
1141
1450
  this.intersectionObserver = null;
1142
1451
  }
1452
+ this.attentionQuality.destroy();
1143
1453
  this.sectionStates.clear();
1144
1454
  this.xpathCache.clear();
1145
1455
  this.pendingEvents = [];
@@ -5869,6 +6179,15 @@ var Grain = (() => {
5869
6179
  resetEventCountSinceLastHeartbeat() {
5870
6180
  this.eventCountSinceLastHeartbeat = 0;
5871
6181
  }
6182
+ /**
6183
+ * Get the activity detector (for internal use by tracking managers)
6184
+ */
6185
+ getActivityDetector() {
6186
+ if (!this.activityDetector) {
6187
+ throw new Error("Activity detector not initialized");
6188
+ }
6189
+ return this.activityDetector;
6190
+ }
5872
6191
  /**
5873
6192
  * Get the effective user ID (public method)
5874
6193
  */