@grainql/analytics-web 3.2.1 → 3.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -179,6 +179,12 @@ class HeatmapTrackingManager {
179
179
  async captureSnapshot() {
180
180
  if (this.snapshotCaptured || !this.snapshotEnabled)
181
181
  return;
182
+ // Check daily snapshot limits before capturing
183
+ if (!this.canUploadSnapshot()) {
184
+ this.log('Snapshot upload limit reached or URL already captured today');
185
+ this.snapshotCaptured = true; // Prevent retry
186
+ return;
187
+ }
182
188
  try {
183
189
  this.log('Capturing DOM snapshot...');
184
190
  // Dynamically import rrweb-snapshot (only if enabled)
@@ -246,11 +252,97 @@ class HeatmapTrackingManager {
246
252
  }
247
253
  const result = await response.json();
248
254
  this.log('Snapshot uploaded successfully:', result);
255
+ // Record successful upload
256
+ this.recordSnapshotUpload(pageUrl);
249
257
  }
250
258
  catch (error) {
251
259
  this.log('Failed to upload snapshot:', error);
252
260
  }
253
261
  }
262
+ /**
263
+ * Check if snapshot can be uploaded based on daily limits
264
+ * - Max 5 snapshots per user per day
265
+ * - Same URL (without query params) can't be uploaded twice in same day
266
+ */
267
+ canUploadSnapshot() {
268
+ if (typeof window === 'undefined' || typeof localStorage === 'undefined') {
269
+ return true; // Allow in SSR/non-browser environments
270
+ }
271
+ try {
272
+ const pageUrl = this.normalizeUrl(window.location.href);
273
+ const urlWithoutQuery = this.stripQueryParams(pageUrl);
274
+ const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
275
+ const storageKey = '_grain_snapshots';
276
+ // Get existing snapshot records
277
+ const stored = localStorage.getItem(storageKey);
278
+ let snapshots = stored ? JSON.parse(stored) : [];
279
+ // Clean up old entries (older than 2 days)
280
+ const twoDaysAgo = Date.now() - (2 * 24 * 60 * 60 * 1000);
281
+ snapshots = snapshots.filter(s => s.timestamp > twoDaysAgo);
282
+ // Get today's snapshots
283
+ const todaySnapshots = snapshots.filter(s => s.date === today);
284
+ // Check daily limit (5 per day)
285
+ if (todaySnapshots.length >= 5) {
286
+ this.log('Daily snapshot limit reached (5/5)');
287
+ return false;
288
+ }
289
+ // Check if this URL (without query params) was already uploaded today
290
+ const urlAlreadyUploaded = todaySnapshots.some(s => this.stripQueryParams(s.url) === urlWithoutQuery);
291
+ if (urlAlreadyUploaded) {
292
+ this.log(`Snapshot for ${urlWithoutQuery} already uploaded today`);
293
+ return false;
294
+ }
295
+ this.log(`Snapshot upload allowed (${todaySnapshots.length}/5 today)`);
296
+ return true;
297
+ }
298
+ catch (error) {
299
+ this.log('Error checking snapshot limits:', error);
300
+ return true; // Allow on error to not break functionality
301
+ }
302
+ }
303
+ /**
304
+ * Record a successful snapshot upload
305
+ */
306
+ recordSnapshotUpload(pageUrl) {
307
+ if (typeof window === 'undefined' || typeof localStorage === 'undefined') {
308
+ return;
309
+ }
310
+ try {
311
+ const today = new Date().toISOString().split('T')[0];
312
+ const storageKey = '_grain_snapshots';
313
+ // Get existing records
314
+ const stored = localStorage.getItem(storageKey);
315
+ let snapshots = stored ? JSON.parse(stored) : [];
316
+ // Add new record
317
+ snapshots.push({
318
+ url: pageUrl,
319
+ date: today,
320
+ timestamp: Date.now()
321
+ });
322
+ // Clean up old entries (older than 2 days)
323
+ const twoDaysAgo = Date.now() - (2 * 24 * 60 * 60 * 1000);
324
+ snapshots = snapshots.filter(s => s.timestamp > twoDaysAgo);
325
+ // Save back to localStorage
326
+ localStorage.setItem(storageKey, JSON.stringify(snapshots));
327
+ this.log(`Snapshot upload recorded: ${snapshots.filter(s => s.date === today).length}/5 today`);
328
+ }
329
+ catch (error) {
330
+ this.log('Error recording snapshot upload:', error);
331
+ }
332
+ }
333
+ /**
334
+ * Strip query parameters from URL for comparison
335
+ */
336
+ stripQueryParams(url) {
337
+ try {
338
+ const urlObj = new URL(url);
339
+ return `${urlObj.origin}${urlObj.pathname}`;
340
+ }
341
+ catch {
342
+ // If URL parsing fails, remove everything after ?
343
+ return url.split('?')[0];
344
+ }
345
+ }
254
346
  /**
255
347
  * Get API URL from tracker configuration
256
348
  */
@@ -1,4 +1,4 @@
1
- /* Grain Analytics Web SDK v3.2.1 | MIT License | Development Build */
1
+ /* Grain Analytics Web SDK v3.2.2 | MIT License | Development Build */
2
2
  "use strict";
3
3
  var Grain = (() => {
4
4
  var __defProp = Object.defineProperty;
@@ -5955,6 +5955,11 @@ var Grain = (() => {
5955
5955
  async captureSnapshot() {
5956
5956
  if (this.snapshotCaptured || !this.snapshotEnabled)
5957
5957
  return;
5958
+ if (!this.canUploadSnapshot()) {
5959
+ this.log("Snapshot upload limit reached or URL already captured today");
5960
+ this.snapshotCaptured = true;
5961
+ return;
5962
+ }
5958
5963
  try {
5959
5964
  this.log("Capturing DOM snapshot...");
5960
5965
  const rrwebSnapshot = await Promise.resolve().then(() => (init_rrweb_snapshot(), rrweb_snapshot_exports));
@@ -6014,10 +6019,84 @@ var Grain = (() => {
6014
6019
  }
6015
6020
  const result2 = await response.json();
6016
6021
  this.log("Snapshot uploaded successfully:", result2);
6022
+ this.recordSnapshotUpload(pageUrl);
6017
6023
  } catch (error) {
6018
6024
  this.log("Failed to upload snapshot:", error);
6019
6025
  }
6020
6026
  }
6027
+ /**
6028
+ * Check if snapshot can be uploaded based on daily limits
6029
+ * - Max 5 snapshots per user per day
6030
+ * - Same URL (without query params) can't be uploaded twice in same day
6031
+ */
6032
+ canUploadSnapshot() {
6033
+ if (typeof window === "undefined" || typeof localStorage === "undefined") {
6034
+ return true;
6035
+ }
6036
+ try {
6037
+ const pageUrl = this.normalizeUrl(window.location.href);
6038
+ const urlWithoutQuery = this.stripQueryParams(pageUrl);
6039
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
6040
+ const storageKey = "_grain_snapshots";
6041
+ const stored = localStorage.getItem(storageKey);
6042
+ let snapshots = stored ? JSON.parse(stored) : [];
6043
+ const twoDaysAgo = Date.now() - 2 * 24 * 60 * 60 * 1e3;
6044
+ snapshots = snapshots.filter((s) => s.timestamp > twoDaysAgo);
6045
+ const todaySnapshots = snapshots.filter((s) => s.date === today);
6046
+ if (todaySnapshots.length >= 5) {
6047
+ this.log("Daily snapshot limit reached (5/5)");
6048
+ return false;
6049
+ }
6050
+ const urlAlreadyUploaded = todaySnapshots.some(
6051
+ (s) => this.stripQueryParams(s.url) === urlWithoutQuery
6052
+ );
6053
+ if (urlAlreadyUploaded) {
6054
+ this.log(`Snapshot for ${urlWithoutQuery} already uploaded today`);
6055
+ return false;
6056
+ }
6057
+ this.log(`Snapshot upload allowed (${todaySnapshots.length}/5 today)`);
6058
+ return true;
6059
+ } catch (error) {
6060
+ this.log("Error checking snapshot limits:", error);
6061
+ return true;
6062
+ }
6063
+ }
6064
+ /**
6065
+ * Record a successful snapshot upload
6066
+ */
6067
+ recordSnapshotUpload(pageUrl) {
6068
+ if (typeof window === "undefined" || typeof localStorage === "undefined") {
6069
+ return;
6070
+ }
6071
+ try {
6072
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
6073
+ const storageKey = "_grain_snapshots";
6074
+ const stored = localStorage.getItem(storageKey);
6075
+ let snapshots = stored ? JSON.parse(stored) : [];
6076
+ snapshots.push({
6077
+ url: pageUrl,
6078
+ date: today,
6079
+ timestamp: Date.now()
6080
+ });
6081
+ const twoDaysAgo = Date.now() - 2 * 24 * 60 * 60 * 1e3;
6082
+ snapshots = snapshots.filter((s) => s.timestamp > twoDaysAgo);
6083
+ localStorage.setItem(storageKey, JSON.stringify(snapshots));
6084
+ this.log(`Snapshot upload recorded: ${snapshots.filter((s) => s.date === today).length}/5 today`);
6085
+ } catch (error) {
6086
+ this.log("Error recording snapshot upload:", error);
6087
+ }
6088
+ }
6089
+ /**
6090
+ * Strip query parameters from URL for comparison
6091
+ */
6092
+ stripQueryParams(url) {
6093
+ try {
6094
+ const urlObj = new URL(url);
6095
+ return `${urlObj.origin}${urlObj.pathname}`;
6096
+ } catch {
6097
+ return url.split("?")[0];
6098
+ }
6099
+ }
6021
6100
  /**
6022
6101
  * Get API URL from tracker configuration
6023
6102
  */