@devskin/browser-sdk 1.0.40 → 1.0.42

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.
@@ -13475,11 +13475,12 @@
13475
13475
  },
13476
13476
  // Configuration options
13477
13477
  sampling: {
13478
- // Mouse interactions
13479
- mousemove: this.config.sampleRate !== undefined
13480
- ? Math.floor(100 / this.config.sampleRate)
13481
- : true,
13482
- // Mouse interactions
13478
+ // Mouse movement sampling - throttle to reduce bandwidth
13479
+ // e.g. 0.1 = capture 10% of mouse movements (Math.floor(1/0.1) = 10, meaning capture 1 every 10 events)
13480
+ mousemove: this.config.mouseMoveSampleRate !== undefined
13481
+ ? Math.max(1, Math.floor(1 / this.config.mouseMoveSampleRate))
13482
+ : 10, // Default: capture 1 every 10 mouse movements (10%)
13483
+ // Mouse interactions (clicks, etc) - always capture
13483
13484
  mouseInteraction: true,
13484
13485
  // Scroll events
13485
13486
  scroll: 150, // Throttle scroll events to every 150ms
@@ -13880,6 +13881,7 @@
13880
13881
  this.anonymousId = null;
13881
13882
  this.sessionStartTime = 0;
13882
13883
  this.initialized = false;
13884
+ this.initializing = false;
13883
13885
  this.heartbeatInterval = null;
13884
13886
  // Collectors
13885
13887
  this.deviceCollector = null;
@@ -13893,14 +13895,83 @@
13893
13895
  // private sessionRecorder: SessionRecorder | null = null; // Replaced by RRWebRecorder
13894
13896
  this.rrwebRecorder = null;
13895
13897
  }
13898
+ /**
13899
+ * Detect if the current visitor is a bot/crawler
13900
+ */
13901
+ isBot() {
13902
+ if (typeof navigator === 'undefined')
13903
+ return true;
13904
+ const botPatterns = [
13905
+ /bot/i,
13906
+ /crawler/i,
13907
+ /spider/i,
13908
+ /crawling/i,
13909
+ /google/i,
13910
+ /bing/i,
13911
+ /yahoo/i,
13912
+ /duckduckgo/i,
13913
+ /baiduspider/i,
13914
+ /yandex/i,
13915
+ /facebookexternalhit/i,
13916
+ /twitterbot/i,
13917
+ /rogerbot/i,
13918
+ /linkedinbot/i,
13919
+ /embedly/i,
13920
+ /quora link preview/i,
13921
+ /showyoubot/i,
13922
+ /outbrain/i,
13923
+ /pinterest/i,
13924
+ /slackbot/i,
13925
+ /vkShare/i,
13926
+ /W3C_Validator/i,
13927
+ /redditbot/i,
13928
+ /Applebot/i,
13929
+ /WhatsApp/i,
13930
+ /flipboard/i,
13931
+ /tumblr/i,
13932
+ /bitlybot/i,
13933
+ /SkypeUriPreview/i,
13934
+ /nuzzel/i,
13935
+ /Discordbot/i,
13936
+ /Google Page Speed/i,
13937
+ /Qwantify/i,
13938
+ /pinterestbot/i,
13939
+ /Bitrix link preview/i,
13940
+ /XING-contenttabreceiver/i,
13941
+ /Chrome-Lighthouse/i,
13942
+ /telegrambot/i,
13943
+ /Lighthouse/i,
13944
+ /GTmetrix/i,
13945
+ /PageSpeed/i,
13946
+ /HeadlessChrome/i,
13947
+ /PhantomJS/i,
13948
+ ];
13949
+ const userAgent = navigator.userAgent;
13950
+ // Check user agent against bot patterns
13951
+ if (botPatterns.some(pattern => pattern.test(userAgent))) {
13952
+ return true;
13953
+ }
13954
+ // Check for headless browsers
13955
+ if ('webdriver' in navigator && navigator.webdriver === true) {
13956
+ return true;
13957
+ }
13958
+ // Check for headless Chrome
13959
+ if (userAgent.includes('HeadlessChrome')) {
13960
+ return true;
13961
+ }
13962
+ return false;
13963
+ }
13896
13964
  /**
13897
13965
  * Initialize the DevSkin SDK
13966
+ * Uses requestIdleCallback to defer heavy initialization without blocking the page
13898
13967
  */
13899
13968
  init(config) {
13900
- if (this.initialized) {
13901
- console.warn('[DevSkin] SDK already initialized');
13969
+ if (this.initialized || this.initializing) {
13970
+ console.warn('[DevSkin] SDK already initialized or initializing');
13902
13971
  return;
13903
13972
  }
13973
+ // Mark as initializing to prevent duplicate init() calls
13974
+ this.initializing = true;
13904
13975
  this.config = Object.assign({ debug: false, captureWebVitals: true, captureNetworkRequests: true, captureErrors: true, captureUserAgent: true, captureLocation: true, captureDevice: true, heatmapOptions: {
13905
13976
  enabled: true,
13906
13977
  trackClicks: true,
@@ -13910,99 +13981,135 @@
13910
13981
  if (this.config.debug) {
13911
13982
  console.log('[DevSkin] Initializing SDK with config:', this.config);
13912
13983
  }
13913
- // Initialize transport
13984
+ // Initialize lightweight components immediately (needed for session context)
13914
13985
  this.transport = new Transport(this.config);
13915
- // Generate anonymous ID if not exists
13916
13986
  this.anonymousId = this.getOrCreateAnonymousId();
13917
- // Initialize collectors BEFORE starting session (so getContextData works)
13918
13987
  this.deviceCollector = new DeviceCollector(this.config);
13919
13988
  this.locationCollector = new LocationCollector(this.config);
13920
13989
  this.browserCollector = new BrowserCollector(this.config);
13921
- // Start session (will now include device/browser/location data)
13922
- // Wait for session creation to complete before starting collectors
13923
- this.startSession().then(() => {
13924
- // Session created, now safe to start collectors that send data
13925
- var _a, _b;
13926
- if (this.config.captureWebVitals) {
13927
- this.performanceCollector = new PerformanceCollector(this.config, this.transport);
13928
- this.performanceCollector.start();
13929
- }
13930
- if (this.config.captureErrors) {
13931
- this.errorCollector = new ErrorCollector(this.config, this.transport);
13932
- this.errorCollector.start();
13933
- }
13934
- if (this.config.captureNetworkRequests) {
13935
- this.networkCollector = new NetworkCollector(this.config, this.transport);
13936
- this.networkCollector.start();
13937
- }
13938
- // Initialize heatmap collector - SEMPRE habilitado
13939
- // Merge default heatmap config with user config
13940
- const heatmapConfig = Object.assign({ enabled: true, trackClicks: true, trackScroll: true, trackMouseMovement: true, mouseMoveSampling: 0.1 }, this.config.heatmapOptions);
13941
- this.config.heatmapOptions = heatmapConfig;
13942
- this.heatmapCollector = new HeatmapCollector(this.config, this.transport, this.anonymousId, this.sessionId);
13943
- this.heatmapCollector.start();
13944
- // Initialize screenshot collector and capture page
13945
- this.screenshotCollector = new ScreenshotCollector(this.config, this.transport);
13946
- this.screenshotCollector.captureAndSend(this.sessionId, window.location.href);
13947
- if (this.config.debug) {
13948
- console.log('[DevSkin] Heatmap collection enabled (always on)');
13949
- }
13950
- // Initialize session recording with rrweb
13951
- if ((_a = this.config.sessionRecording) === null || _a === void 0 ? void 0 : _a.enabled) {
13952
- // Use RRWebRecorder for complete DOM recording
13953
- // Pass sessionStartTime to ensure timestamp continuity across page navigations
13954
- this.rrwebRecorder = new RRWebRecorder(this.sessionId, {
13955
- enabled: true,
13956
- sampleRate: this.config.sessionRecording.sampling || 0.5,
13957
- blockClass: 'rr-block',
13958
- ignoreClass: this.config.sessionRecording.ignoreClass || 'rr-ignore',
13959
- maskAllInputs: this.config.sessionRecording.maskAllInputs !== undefined
13960
- ? this.config.sessionRecording.maskAllInputs
13961
- : true,
13962
- maskInputOptions: {
13963
- password: true,
13964
- email: true,
13965
- tel: true,
13966
- },
13967
- recordCanvas: this.config.sessionRecording.recordCanvas || false,
13968
- collectFonts: true,
13969
- inlineStylesheet: true,
13970
- checkoutEveryNms: 5 * 60 * 1000, // Every 5 minutes
13971
- checkoutEveryNth: 200, // Every 200 events
13972
- }, (events) => {
13973
- var _a;
13974
- // Send rrweb events to backend
13975
- (_a = this.transport) === null || _a === void 0 ? void 0 : _a.sendRecordingEvents(this.sessionId, events);
13976
- }, this.sessionStartTime // Pass session start time for timestamp continuity
13977
- );
13978
- // Start recording immediately (session already created)
13979
- this.rrwebRecorder.start();
13980
- if ((_b = this.config) === null || _b === void 0 ? void 0 : _b.debug) {
13981
- console.log('[DevSkin] RRWeb recording started for session:', this.sessionId);
13990
+ // Defer heavy initialization to avoid blocking page load/rendering
13991
+ const initHeavyCollectors = () => {
13992
+ // Start session (will now include device/browser/location data)
13993
+ // Wait for session creation to complete before starting collectors
13994
+ this.startSession().then(() => {
13995
+ // Session created, now safe to start collectors that send data
13996
+ var _a, _b, _c;
13997
+ if (this.config.captureWebVitals) {
13998
+ this.performanceCollector = new PerformanceCollector(this.config, this.transport);
13999
+ this.performanceCollector.start();
14000
+ }
14001
+ if (this.config.captureErrors) {
14002
+ this.errorCollector = new ErrorCollector(this.config, this.transport);
14003
+ this.errorCollector.start();
14004
+ }
14005
+ if (this.config.captureNetworkRequests) {
14006
+ this.networkCollector = new NetworkCollector(this.config, this.transport);
14007
+ this.networkCollector.start();
14008
+ }
14009
+ // Initialize heatmap collector - SEMPRE habilitado
14010
+ // Merge default heatmap config with user config
14011
+ const heatmapConfig = Object.assign({ enabled: true, trackClicks: true, trackScroll: true, trackMouseMovement: true, mouseMoveSampling: 0.1 }, this.config.heatmapOptions);
14012
+ this.config.heatmapOptions = heatmapConfig;
14013
+ this.heatmapCollector = new HeatmapCollector(this.config, this.transport, this.anonymousId, this.sessionId);
14014
+ this.heatmapCollector.start();
14015
+ // Initialize screenshot collector and capture page
14016
+ this.screenshotCollector = new ScreenshotCollector(this.config, this.transport);
14017
+ this.screenshotCollector.captureAndSend(this.sessionId, window.location.href);
14018
+ if (this.config.debug) {
14019
+ console.log('[DevSkin] Heatmap collection enabled (always on)');
13982
14020
  }
14021
+ // Initialize session recording with rrweb
14022
+ if ((_a = this.config.sessionRecording) === null || _a === void 0 ? void 0 : _a.enabled) {
14023
+ // Skip recording for bots/crawlers to avoid impacting SEO
14024
+ const isBot = this.isBot();
14025
+ // Apply session sampling - decide if this session should be recorded
14026
+ const samplingRate = this.config.sessionRecording.sampling || 1.0;
14027
+ const shouldRecord = !isBot && Math.random() < samplingRate;
14028
+ if (shouldRecord) {
14029
+ // Use RRWebRecorder for complete DOM recording
14030
+ // Pass sessionStartTime to ensure timestamp continuity across page navigations
14031
+ this.rrwebRecorder = new RRWebRecorder(this.sessionId, {
14032
+ enabled: true,
14033
+ mouseMoveSampleRate: 0.5, // Sample 10% of mouse movements to reduce bandwidth
14034
+ blockClass: 'rr-block',
14035
+ ignoreClass: this.config.sessionRecording.ignoreClass || 'rr-ignore',
14036
+ maskAllInputs: this.config.sessionRecording.maskAllInputs !== undefined
14037
+ ? this.config.sessionRecording.maskAllInputs
14038
+ : true,
14039
+ maskInputOptions: {
14040
+ password: true,
14041
+ email: true,
14042
+ tel: true,
14043
+ },
14044
+ recordCanvas: this.config.sessionRecording.recordCanvas || false,
14045
+ collectFonts: true,
14046
+ inlineStylesheet: true,
14047
+ checkoutEveryNms: 5 * 60 * 1000, // Every 5 minutes
14048
+ checkoutEveryNth: 200, // Every 200 events
14049
+ }, (events) => {
14050
+ var _a;
14051
+ // Send rrweb events to backend
14052
+ (_a = this.transport) === null || _a === void 0 ? void 0 : _a.sendRecordingEvents(this.sessionId, events);
14053
+ }, this.sessionStartTime // Pass session start time for timestamp continuity
14054
+ );
14055
+ // Start recording immediately (session already created)
14056
+ this.rrwebRecorder.start();
14057
+ if ((_b = this.config) === null || _b === void 0 ? void 0 : _b.debug) {
14058
+ console.log(`[DevSkin] RRWeb recording started for session: ${this.sessionId} (sampling: ${samplingRate * 100}%)`);
14059
+ }
14060
+ }
14061
+ else {
14062
+ if ((_c = this.config) === null || _c === void 0 ? void 0 : _c.debug) {
14063
+ if (isBot) {
14064
+ console.log(`[DevSkin] Session ${this.sessionId} skipped recording (bot/crawler detected for SEO optimization)`);
14065
+ }
14066
+ else {
14067
+ console.log(`[DevSkin] Session ${this.sessionId} not sampled for recording (sampling: ${samplingRate * 100}%)`);
14068
+ }
14069
+ }
14070
+ }
14071
+ }
14072
+ // Track initial page view
14073
+ this.trackPageView();
14074
+ // Start heartbeat to update session duration every 30 seconds
14075
+ this.startHeartbeat();
14076
+ }).catch((err) => {
14077
+ console.error('[DevSkin] Failed to create session:', err);
14078
+ });
14079
+ // Track page visibility changes
14080
+ this.setupVisibilityTracking();
14081
+ // Track page unload
14082
+ this.setupUnloadTracking();
14083
+ // Mark as fully initialized only after everything is loaded
14084
+ this.initialized = true;
14085
+ this.initializing = false;
14086
+ };
14087
+ // Use requestIdleCallback to defer heavy initialization (non-blocking)
14088
+ // Falls back to setTimeout for browsers that don't support it
14089
+ if (typeof window !== 'undefined') {
14090
+ if ('requestIdleCallback' in window) {
14091
+ window.requestIdleCallback(initHeavyCollectors, { timeout: 2000 });
13983
14092
  }
13984
- // Track initial page view
13985
- this.trackPageView();
13986
- // Start heartbeat to update session duration every 30 seconds
13987
- this.startHeartbeat();
13988
- }).catch((err) => {
13989
- console.error('[DevSkin] Failed to create session:', err);
13990
- });
13991
- // Track page visibility changes
13992
- this.setupVisibilityTracking();
13993
- // Track page unload
13994
- this.setupUnloadTracking();
13995
- this.initialized = true;
14093
+ else {
14094
+ // Fallback for older browsers
14095
+ setTimeout(initHeavyCollectors, 1);
14096
+ }
14097
+ }
14098
+ else {
14099
+ // Node.js environment (SSR)
14100
+ initHeavyCollectors();
14101
+ }
13996
14102
  if (this.config.debug) {
13997
- console.log('[DevSkin] SDK initialized successfully');
14103
+ console.log('[DevSkin] SDK initialization started (heavy collectors loading in background)');
13998
14104
  }
13999
14105
  }
14000
14106
  /**
14001
14107
  * Track a custom event
14108
+ * Works immediately after init() even if heavy collectors are still loading
14002
14109
  */
14003
14110
  track(eventName, properties) {
14004
- var _a, _b;
14005
- if (!this.initialized) {
14111
+ var _a, _b, _c, _d;
14112
+ if (!this.transport) {
14006
14113
  console.warn('[DevSkin] SDK not initialized. Call init() first.');
14007
14114
  return;
14008
14115
  }
@@ -14010,15 +14117,15 @@
14010
14117
  eventName: eventName,
14011
14118
  eventType: 'track',
14012
14119
  timestamp: new Date().toISOString(),
14013
- sessionId: this.sessionId,
14014
- userId: this.userId || undefined,
14015
- anonymousId: this.anonymousId || undefined,
14120
+ sessionId: (_a = this.sessionId) !== null && _a !== void 0 ? _a : undefined,
14121
+ userId: (_b = this.userId) !== null && _b !== void 0 ? _b : undefined,
14122
+ anonymousId: (_c = this.anonymousId) !== null && _c !== void 0 ? _c : undefined,
14016
14123
  properties: Object.assign(Object.assign({}, properties), this.getContextData()),
14017
14124
  pageUrl: window.location.href,
14018
14125
  pageTitle: document.title,
14019
14126
  };
14020
- (_a = this.transport) === null || _a === void 0 ? void 0 : _a.sendEvent(eventData);
14021
- if ((_b = this.config) === null || _b === void 0 ? void 0 : _b.debug) {
14127
+ this.transport.sendEvent(eventData);
14128
+ if ((_d = this.config) === null || _d === void 0 ? void 0 : _d.debug) {
14022
14129
  console.log('[DevSkin] Event tracked:', eventData);
14023
14130
  }
14024
14131
  }
@@ -14047,23 +14154,24 @@
14047
14154
  }
14048
14155
  /**
14049
14156
  * Identify a user
14157
+ * Works immediately after init() even if heavy collectors are still loading
14050
14158
  */
14051
14159
  identify(userId, traits) {
14052
- var _a, _b;
14053
- if (!this.initialized) {
14160
+ var _a, _b, _c;
14161
+ if (!this.transport) {
14054
14162
  console.warn('[DevSkin] SDK not initialized. Call init() first.');
14055
14163
  return;
14056
14164
  }
14057
14165
  this.userId = userId;
14058
14166
  const userData = {
14059
14167
  userId: userId,
14060
- anonymousId: this.anonymousId || undefined,
14168
+ anonymousId: (_a = this.anonymousId) !== null && _a !== void 0 ? _a : undefined,
14061
14169
  traits: Object.assign(Object.assign({}, traits), this.getContextData()),
14062
- sessionId: this.sessionId,
14170
+ sessionId: (_b = this.sessionId) !== null && _b !== void 0 ? _b : undefined,
14063
14171
  timestamp: new Date().toISOString(),
14064
14172
  };
14065
- (_a = this.transport) === null || _a === void 0 ? void 0 : _a.identifyUser(userData);
14066
- if ((_b = this.config) === null || _b === void 0 ? void 0 : _b.debug) {
14173
+ this.transport.identifyUser(userData);
14174
+ if ((_c = this.config) === null || _c === void 0 ? void 0 : _c.debug) {
14067
14175
  console.log('[DevSkin] User identified:', userData);
14068
14176
  }
14069
14177
  }