@openreplay/tracker 16.2.1 → 16.3.0-beta.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.
@@ -13,6 +13,7 @@ import type { Options as TimingOptions } from './modules/timing.js';
13
13
  import type { Options as NetworkOptions } from './modules/network.js';
14
14
  import type { MouseHandlerOptions } from './modules/mouse.js';
15
15
  import type { SessionInfo } from './app/session.js';
16
+ import type { CssRulesOptions } from './modules/cssrules.js';
16
17
  import type { StartOptions } from './app/index.js';
17
18
  import type { StartPromiseReturn } from './app/index.js';
18
19
  export type Options = Partial<AppOptions & ConsoleOptions & ExceptionOptions & InputOptions & PerformanceOptions & TimingOptions> & {
@@ -28,6 +29,7 @@ export type Options = Partial<AppOptions & ConsoleOptions & ExceptionOptions & I
28
29
  onFlagsLoad?: (flags: IFeatureFlag[]) => void;
29
30
  };
30
31
  __DISABLE_SECURE_MODE?: boolean;
32
+ css: CssRulesOptions;
31
33
  };
32
34
  export default class API {
33
35
  readonly options: Partial<Options>;
@@ -1,2 +1,16 @@
1
1
  import type App from '../app/index.js';
2
- export default function (app: App | null): void;
2
+ export interface CssRulesOptions {
3
+ checkCssInterval?: number;
4
+ scanInMemoryCSS?: boolean;
5
+ /**
6
+ Useful for cases where you expect limited amount of mutations
7
+
8
+ i.e when sheets are hydrated on client after initial render.
9
+
10
+ if you want to observe for x seconds, do (x*1000)/checkCssInterval = checkLimit
11
+
12
+ applied to each stylesheet individually.
13
+ */
14
+ checkLimit?: number;
15
+ }
16
+ export default function (app: App, opts: CssRulesOptions): void;
package/dist/lib/entry.js CHANGED
@@ -3560,7 +3560,7 @@ function isNodeStillActive(node) {
3560
3560
  return [false, e];
3561
3561
  }
3562
3562
  }
3563
- const defaults = {
3563
+ const defaults$1 = {
3564
3564
  interval: SECOND * 30,
3565
3565
  batchSize: 2500,
3566
3566
  enabled: true,
@@ -3588,7 +3588,7 @@ class Maintainer {
3588
3588
  clearInterval(this.interval);
3589
3589
  }
3590
3590
  };
3591
- this.options = { ...defaults, ...options };
3591
+ this.options = { ...defaults$1, ...options };
3592
3592
  }
3593
3593
  }
3594
3594
 
@@ -5202,7 +5202,7 @@ class App {
5202
5202
  this.stopCallbacks = [];
5203
5203
  this.commitCallbacks = [];
5204
5204
  this.activityState = ActivityState.NotActive;
5205
- this.version = '16.2.1'; // TODO: version compatability check inside each plugin.
5205
+ this.version = '16.3.0-beta.0'; // TODO: version compatability check inside each plugin.
5206
5206
  this.socketMode = false;
5207
5207
  this.compressionThreshold = 24 * 1000;
5208
5208
  this.bc = null;
@@ -8037,60 +8037,137 @@ function Viewport (app) {
8037
8037
  app.ticker.attach(sendSetViewportSize, 5, false);
8038
8038
  }
8039
8039
 
8040
- function CSSRules (app) {
8041
- if (app === null) {
8040
+ const defaults = {
8041
+ checkCssInterval: 200,
8042
+ scanInMemoryCSS: false,
8043
+ checkLimit: undefined,
8044
+ };
8045
+ function CSSRules (app, opts) {
8046
+ if (app === null)
8042
8047
  return;
8043
- }
8044
8048
  if (!window.CSSStyleSheet) {
8045
8049
  app.send(TechnicalInfo('no_stylesheet_prototype_in_window', ''));
8046
8050
  return;
8047
8051
  }
8052
+ const options = { ...defaults, ...opts };
8053
+ // sheetID:index -> ruleText
8054
+ const ruleSnapshots = new Map();
8055
+ let checkInterval = null;
8056
+ const trackedSheets = new Set();
8057
+ const checkIntervalMs = options.checkCssInterval || 200;
8058
+ let checkIterations = {};
8059
+ function checkRuleChanges() {
8060
+ if (!options.scanInMemoryCSS)
8061
+ return;
8062
+ const allSheets = trackedSheets.values();
8063
+ for (const sheet of allSheets) {
8064
+ try {
8065
+ const sheetID = styleSheetIDMap.get(sheet);
8066
+ if (!sheetID)
8067
+ continue;
8068
+ if (options.checkLimit) {
8069
+ if (!checkIterations[sheetID]) {
8070
+ checkIterations[sheetID] = 0;
8071
+ }
8072
+ else {
8073
+ checkIterations[sheetID]++;
8074
+ }
8075
+ if (checkIterations[sheetID] > options.checkLimit) {
8076
+ trackedSheets.delete(sheet);
8077
+ return;
8078
+ }
8079
+ }
8080
+ for (let j = 0; j < sheet.cssRules.length; j++) {
8081
+ try {
8082
+ const rule = sheet.cssRules[j];
8083
+ const key = `${sheetID}:${j}`;
8084
+ const oldText = ruleSnapshots.get(key);
8085
+ const newText = rule.cssText;
8086
+ if (oldText !== newText) {
8087
+ if (oldText !== undefined) {
8088
+ // Rule is changed
8089
+ app.send(AdoptedSSDeleteRule(sheetID, j));
8090
+ app.send(AdoptedSSInsertRuleURLBased(sheetID, newText, j, app.getBaseHref()));
8091
+ }
8092
+ else {
8093
+ // Rule added
8094
+ app.send(AdoptedSSInsertRuleURLBased(sheetID, newText, j, app.getBaseHref()));
8095
+ }
8096
+ ruleSnapshots.set(key, newText);
8097
+ }
8098
+ }
8099
+ catch (e) {
8100
+ /* Skip inaccessible rules */
8101
+ }
8102
+ }
8103
+ const keysToCheck = Array.from(ruleSnapshots.keys()).filter((key) => key.startsWith(`${sheetID}:`));
8104
+ for (const key of keysToCheck) {
8105
+ const index = parseInt(key.split(':')[1], 10);
8106
+ if (index >= sheet.cssRules.length) {
8107
+ ruleSnapshots.delete(key);
8108
+ }
8109
+ }
8110
+ }
8111
+ catch (e) {
8112
+ /* Skip inaccessible sheets */
8113
+ trackedSheets.delete(sheet);
8114
+ }
8115
+ }
8116
+ }
8117
+ const emptyRuleReg = /{\s*}/;
8118
+ function isRuleEmpty(rule) {
8119
+ return emptyRuleReg.test(rule);
8120
+ }
8048
8121
  const sendInsertDeleteRule = app.safe((sheet, index, rule) => {
8049
8122
  const sheetID = styleSheetIDMap.get(sheet);
8050
- if (!sheetID) {
8051
- // OK-case. Sheet haven't been registered yet. Rules will be sent on registration.
8123
+ if (!sheetID)
8052
8124
  return;
8053
- }
8054
8125
  if (typeof rule === 'string') {
8055
8126
  app.send(AdoptedSSInsertRuleURLBased(sheetID, rule, index, app.getBaseHref()));
8127
+ if (isRuleEmpty(rule)) {
8128
+ ruleSnapshots.set(`${sheetID}:${index}`, rule);
8129
+ trackedSheets.add(sheet);
8130
+ }
8056
8131
  }
8057
8132
  else {
8058
8133
  app.send(AdoptedSSDeleteRule(sheetID, index));
8134
+ if (ruleSnapshots.has(`${sheetID}:${index}`)) {
8135
+ ruleSnapshots.delete(`${sheetID}:${index}`);
8136
+ }
8059
8137
  }
8060
8138
  });
8061
- // TODO: proper rule insertion/removal (how?)
8062
8139
  const sendReplaceGroupingRule = app.safe((rule) => {
8063
8140
  let topmostRule = rule;
8064
- while (topmostRule.parentRule) {
8141
+ while (topmostRule.parentRule)
8065
8142
  topmostRule = topmostRule.parentRule;
8066
- }
8067
8143
  const sheet = topmostRule.parentStyleSheet;
8068
- if (!sheet) {
8069
- app.debug.warn('No parent StyleSheet found for', topmostRule, rule);
8144
+ if (!sheet)
8070
8145
  return;
8071
- }
8072
8146
  const sheetID = styleSheetIDMap.get(sheet);
8073
- if (!sheetID) {
8074
- app.debug.warn('No sheedID found for', sheet, styleSheetIDMap);
8147
+ if (!sheetID)
8075
8148
  return;
8076
- }
8077
8149
  const cssText = topmostRule.cssText;
8078
- const ruleList = sheet.cssRules;
8079
- const idx = Array.from(ruleList).indexOf(topmostRule);
8150
+ const idx = Array.from(sheet.cssRules).indexOf(topmostRule);
8080
8151
  if (idx >= 0) {
8081
8152
  app.send(AdoptedSSInsertRuleURLBased(sheetID, cssText, idx, app.getBaseHref()));
8082
- app.send(AdoptedSSDeleteRule(sheetID, idx + 1)); // Remove previous clone
8083
- }
8084
- else {
8085
- app.debug.warn('Rule index not found in', sheet, topmostRule);
8153
+ app.send(AdoptedSSDeleteRule(sheetID, idx + 1));
8154
+ if (isRuleEmpty(cssText)) {
8155
+ ruleSnapshots.set(`${sheetID}:${idx}`, cssText);
8156
+ trackedSheets.add(sheet);
8157
+ }
8086
8158
  }
8087
8159
  });
8160
+ // Patch prototype methods
8088
8161
  const patchContext = app.safe((context) => {
8162
+ if (context.__css_tracking_patched__)
8163
+ return;
8164
+ context.__css_tracking_patched__ = true;
8089
8165
  const { insertRule, deleteRule } = context.CSSStyleSheet.prototype;
8090
8166
  const { insertRule: groupInsertRule, deleteRule: groupDeleteRule } = context.CSSGroupingRule.prototype;
8091
8167
  context.CSSStyleSheet.prototype.insertRule = function (rule, index = 0) {
8092
- sendInsertDeleteRule(this, index, rule);
8093
- return insertRule.call(this, rule, index);
8168
+ const result = insertRule.call(this, rule, index);
8169
+ sendInsertDeleteRule(this, result, rule);
8170
+ return result;
8094
8171
  };
8095
8172
  context.CSSStyleSheet.prototype.deleteRule = function (index) {
8096
8173
  sendInsertDeleteRule(this, index);
@@ -8101,7 +8178,7 @@ function CSSRules (app) {
8101
8178
  sendReplaceGroupingRule(this);
8102
8179
  return result;
8103
8180
  };
8104
- context.CSSGroupingRule.prototype.deleteRule = function (index = 0) {
8181
+ context.CSSGroupingRule.prototype.deleteRule = function (index) {
8105
8182
  const result = groupDeleteRule.call(this, index);
8106
8183
  sendReplaceGroupingRule(this);
8107
8184
  return result;
@@ -8110,24 +8187,38 @@ function CSSRules (app) {
8110
8187
  patchContext(window);
8111
8188
  app.observer.attachContextCallback(patchContext);
8112
8189
  app.nodes.attachNodeCallback((node) => {
8113
- if (!hasTag(node, 'style') || !node.sheet) {
8190
+ if (!hasTag(node, 'style') || !node.sheet)
8191
+ return;
8192
+ if (node.textContent !== null && node.textContent.trim().length > 0)
8114
8193
  return;
8115
- }
8116
- if (node.textContent !== null && node.textContent.trim().length > 0) {
8117
- return; // Non-virtual styles captured by the observer as a text
8118
- }
8119
8194
  const nodeID = app.nodes.getID(node);
8120
- if (!nodeID) {
8195
+ if (!nodeID)
8121
8196
  return;
8122
- }
8123
8197
  const sheet = node.sheet;
8124
8198
  const sheetID = nextID();
8125
8199
  styleSheetIDMap.set(sheet, sheetID);
8126
8200
  app.send(AdoptedSSAddOwner(sheetID, nodeID));
8127
- const rules = sheet.cssRules;
8128
- for (let i = 0; i < rules.length; i++) {
8129
- sendInsertDeleteRule(sheet, i, rules[i].cssText);
8201
+ for (let i = 0; i < sheet.cssRules.length; i++) {
8202
+ try {
8203
+ sendInsertDeleteRule(sheet, i, sheet.cssRules[i].cssText);
8204
+ }
8205
+ catch (e) {
8206
+ // Skip inaccessible rules
8207
+ }
8208
+ }
8209
+ });
8210
+ function startChecking() {
8211
+ if (checkInterval || !options.scanInMemoryCSS)
8212
+ return;
8213
+ checkInterval = window.setInterval(checkRuleChanges, checkIntervalMs);
8214
+ }
8215
+ setTimeout(startChecking, 50);
8216
+ app.attachStopCallback(() => {
8217
+ if (checkInterval) {
8218
+ clearInterval(checkInterval);
8219
+ checkInterval = null;
8130
8220
  }
8221
+ ruleSnapshots.clear();
8131
8222
  });
8132
8223
  }
8133
8224
 
@@ -9692,7 +9783,7 @@ class API {
9692
9783
  this.signalStartIssue = (reason, missingApi) => {
9693
9784
  const doNotTrack = this.checkDoNotTrack();
9694
9785
  console.log("Tracker couldn't start due to:", JSON.stringify({
9695
- trackerVersion: '16.2.1',
9786
+ trackerVersion: '16.3.0-beta.0',
9696
9787
  projectKey: this.options.projectKey,
9697
9788
  doNotTrack,
9698
9789
  reason: missingApi.length ? `missing api: ${missingApi.join(',')}` : reason,
@@ -9791,7 +9882,7 @@ class API {
9791
9882
  Mouse(app, options.mouse);
9792
9883
  // inside iframe, we ignore viewport scroll
9793
9884
  Scroll(app, this.crossdomainMode);
9794
- CSSRules(app);
9885
+ CSSRules(app, options.css);
9795
9886
  ConstructedStyleSheets(app);
9796
9887
  Console(app, options);
9797
9888
  Exception(app, options);