Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ module.exports = {
'eslint-plugin-prefer-arrow',
'eslint-plugin-unicorn',
'@typescript-eslint',
'eslint-plugin-local-rules',
],
rules: {
'@typescript-eslint/array-type': ['error', { default: 'array-simple' }],
Expand Down Expand Up @@ -217,5 +218,12 @@ module.exports = {
'@typescript-eslint/restrict-template-expressions': 'off',
},
},
{
files: ['packages/*/src/**/*.ts'],
excludedFiles: '*.spec.ts',
rules: {
'local-rules/disallow-side-effects': 'error',
},
},
],
}
3 changes: 2 additions & 1 deletion LICENSE-3rdparty.csv
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ dev,emoji-name-map,MIT,Copyright 2016-19 Ionică Bizău <bizauionica@gmail.com>
dev,eslint,MIT,Copyright JS Foundation and other contributors
dev,eslint-config-prettier,MIT,Copyright (c) 2017, 2018, 2019, 2020 Simon Lydell and contributors
dev,eslint-plugin-import,MIT,Copyright (c) 2015 Ben Mosher
dev,eslint-plugin-jsdoc,BSD-3-Clause,Copyright (c) 2018, Gajus Kuizinas (http://gajus.com/)
dev,eslint-plugin-jasmine,MIT,Copyright (c) 2021 Tom Vincent
dev,eslint-plugin-jsdoc,BSD-3-Clause,Copyright (c) 2018, Gajus Kuizinas (http://gajus.com/)
dev,eslint-plugin-local-rules,MIT,Copyright (c) 2017 Clayton Watts
dev,eslint-plugin-prefer-arrow,MIT,Copyright (c) 2018 Triston Jones
dev,eslint-plugin-unicorn,MIT,Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
dev,express,MIT,Copyright 2009-2014 TJ Holowaychuk 2013-2014 Roman Shtylman 2014-2015 Douglas Christopher Wilson
Expand Down
209 changes: 209 additions & 0 deletions eslint-local-rules.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/* eslint-disable unicorn/filename-case */
const path = require('path')

// Declare the local rules used by the Browser SDK
//
// See https://eslint.org/docs/developer-guide/working-with-rules for documentation on how to write
// rules.
//
// You can use https://astexplorer.net/ to explore the parsed data structure of a code snippet.
// Choose '@typescript-eslint/parser' as a parser to have the exact same structure as our ESLint
// parser.
module.exports = {
'disallow-side-effects': {
meta: {
docs: {
description:
'Disallow potential side effects when evaluating modules, to ensure modules content are tree-shakable.',
recommended: false,
},
schema: [],
},
create(context) {
const filename = context.getFilename()
if (pathsWithSideEffect.has(filename)) {
return {}
}
return {
Program(node) {
reportPotentialSideEffect(context, node)
},
}
},
},
}

const packagesRoot = `${__dirname}/packages`

// Those modules are known to have side effects when evaluated
const pathsWithSideEffect = new Set([
`${packagesRoot}/logs/src/boot/logs.entry.ts`,
`${packagesRoot}/logs/src/index.ts`,
`${packagesRoot}/rum-recorder/src/boot/recorder.entry.ts`,
`${packagesRoot}/rum-recorder/src/index.ts`,
`${packagesRoot}/rum/src/boot/rum.entry.ts`,
`${packagesRoot}/rum/src/index.ts`,
])

// Those packages are known to have no side effects when evaluated
const packagesWithoutSideEffect = new Set(['@datadog/browser-core', '@datadog/browser-rum-core'])

/**
* Iterate over the given node and its children, and report any node that may have a side effect
* when evaluated.
*
* @example
* const foo = 1 // OK, this statement can't have any side effect
* foo() // KO, we don't know what 'foo' does, report this
* function bar() { // OK, a function declaration doesn't have side effects
* foo() // OK, this statement won't be executed when evaluating the module code
* }
*/
function reportPotentialSideEffect(context, node) {
// This acts like an authorized list of syntax nodes to use directly in the body of a module. All
// those nodes should not have a side effect when evaluated.
//
// This list is probably not complete, feel free to add more cases if you encounter an unhandled
// node.
switch (node.type) {
case 'Program':
node.body.forEach((child) => reportPotentialSideEffect(context, child))
return
case 'TemplateLiteral':
node.expressions.forEach((child) => reportPotentialSideEffect(context, child))
return
case 'ExportNamedDeclaration':
case 'ExportAllDeclaration':
case 'ImportDeclaration':
if (node.declaration) {
reportPotentialSideEffect(context, node.declaration)
} else if (
node.source &&
node.importKind !== 'type' &&
!isAllowedImport(context.getFilename(), node.source.value)
) {
context.report({
node: node.source,
message: 'This file cannot import modules with side-effects',
})
}
return
case 'VariableDeclaration':
node.declarations.forEach((child) => reportPotentialSideEffect(context, child))
return
case 'VariableDeclarator':
if (node.init) {
reportPotentialSideEffect(context, node.init)
}
return
case 'ArrayExpression':
node.elements.forEach((child) => reportPotentialSideEffect(context, child))
return
case 'UnaryExpression':
reportPotentialSideEffect(context, node.argument)
return
case 'ObjectExpression':
node.properties.forEach((child) => reportPotentialSideEffect(context, child))
return
case 'SpreadElement':
reportPotentialSideEffect(context, node.argument)
return
case 'Property':
reportPotentialSideEffect(context, node.key)
reportPotentialSideEffect(context, node.value)
return
case 'BinaryExpression':
reportPotentialSideEffect(context, node.left)
reportPotentialSideEffect(context, node.right)
return
case 'TSAsExpression':
case 'ExpressionStatement':
reportPotentialSideEffect(context, node.expression)
return
case 'MemberExpression':
reportPotentialSideEffect(context, node.object)
reportPotentialSideEffect(context, node.property)
return
case 'FunctionExpression':
case 'ArrowFunctionExpression':
case 'FunctionDeclaration':
case 'ClassDeclaration':
case 'TSEnumDeclaration':
case 'TSInterfaceDeclaration':
case 'TSTypeAliasDeclaration':
case 'TSDeclareFunction':
case 'Literal':
case 'Identifier':
return
case 'CallExpression':
if (isAllowedCallExpression(node)) {
return
}
case 'NewExpression':
if (isAllowedNewExpression(node)) {
return
}
}

// If the node doesn't match any of the condition above, report it
context.report({
node,
message: `${node.type} can have side effects when the module is evaluated. \
Maybe move it in a function declaration?`,
})
}

/**
* Make sure an 'import' statement does not pull a module or package with side effects.
*/
function isAllowedImport(basePath, source) {
if (source.startsWith('.')) {
const resolvedPath = `${path.resolve(path.dirname(basePath), source)}.ts`
return !pathsWithSideEffect.has(resolvedPath)
}
return packagesWithoutSideEffect.has(source)
}

/* eslint-disable max-len */
/**
* Authorize some call expressions. Feel free to add more exceptions here. Good candidates would
* be functions that are known to be ECMAScript functions without side effects, that are likely to
* be considered as pure functions by the bundler.
*
* You can experiment with Rollup tree-shaking strategy to ensure your function is known to be pure.
* https://rollupjs.org/repl/?version=2.38.5&shareable=JTdCJTIybW9kdWxlcyUyMiUzQSU1QiU3QiUyMm5hbWUlMjIlM0ElMjJtYWluLmpzJTIyJTJDJTIyY29kZSUyMiUzQSUyMiUyRiUyRiUyMFB1cmUlMjBmdW5jdGlvbnMlNUNubmV3JTIwV2Vha01hcCgpJTVDbk9iamVjdC5rZXlzKCklNUNuJTVDbiUyRiUyRiUyMFNpZGUlMjBlZmZlY3QlMjBmdW5jdGlvbnMlNUNuZm9vKCklMjAlMkYlMkYlMjB1bmtub3duJTIwZnVuY3Rpb25zJTIwYXJlJTIwY29uc2lkZXJlZCUyMHRvJTIwaGF2ZSUyMHNpZGUlMjBlZmZlY3RzJTVDbmFsZXJ0KCdhYWEnKSU1Q25uZXclMjBNdXRhdGlvbk9ic2VydmVyKCgpJTIwJTNEJTNFJTIwJTdCJTdEKSUyMiUyQyUyMmlzRW50cnklMjIlM0F0cnVlJTdEJTVEJTJDJTIyb3B0aW9ucyUyMiUzQSU3QiUyMmZvcm1hdCUyMiUzQSUyMmVzJTIyJTJDJTIybmFtZSUyMiUzQSUyMm15QnVuZGxlJTIyJTJDJTIyYW1kJTIyJTNBJTdCJTIyaWQlMjIlM0ElMjIlMjIlN0QlMkMlMjJnbG9iYWxzJTIyJTNBJTdCJTdEJTdEJTJDJTIyZXhhbXBsZSUyMiUzQW51bGwlN0Q=
*
* Webpack is not as smart as Rollup, and it usually treat all call expressions as impure, but it
* could be fine to allow it nonetheless at it pulls very little code.
*/
/* eslint-enable max-len */
function isAllowedCallExpression({ callee }) {
// Allow "Object.keys()"
if (callee.type === 'MemberExpression' && callee.object.name === 'Object' && callee.property.name === 'keys') {
return true
}

return false
}

/* eslint-disable max-len */
/**
* Authorize some 'new' expressions. Feel free to add more exceptions here. Good candidates would
* be functions that are known to be ECMAScript functions without side effects, that are likely to
* be considered as pure functions by the bundler.
*
* You can experiment with Rollup tree-shaking strategy to ensure your function is known to be pure.
* https://rollupjs.org/repl/?version=2.38.5&shareable=JTdCJTIybW9kdWxlcyUyMiUzQSU1QiU3QiUyMm5hbWUlMjIlM0ElMjJtYWluLmpzJTIyJTJDJTIyY29kZSUyMiUzQSUyMiUyRiUyRiUyMFB1cmUlMjBmdW5jdGlvbnMlNUNubmV3JTIwV2Vha01hcCgpJTVDbk9iamVjdC5rZXlzKCklNUNuJTVDbiUyRiUyRiUyMFNpZGUlMjBlZmZlY3QlMjBmdW5jdGlvbnMlNUNuZm9vKCklMjAlMkYlMkYlMjB1bmtub3duJTIwZnVuY3Rpb25zJTIwYXJlJTIwY29uc2lkZXJlZCUyMHRvJTIwaGF2ZSUyMHNpZGUlMjBlZmZlY3RzJTVDbmFsZXJ0KCdhYWEnKSU1Q25uZXclMjBNdXRhdGlvbk9ic2VydmVyKCgpJTIwJTNEJTNFJTIwJTdCJTdEKSUyMiUyQyUyMmlzRW50cnklMjIlM0F0cnVlJTdEJTVEJTJDJTIyb3B0aW9ucyUyMiUzQSU3QiUyMmZvcm1hdCUyMiUzQSUyMmVzJTIyJTJDJTIybmFtZSUyMiUzQSUyMm15QnVuZGxlJTIyJTJDJTIyYW1kJTIyJTNBJTdCJTIyaWQlMjIlM0ElMjIlMjIlN0QlMkMlMjJnbG9iYWxzJTIyJTNBJTdCJTdEJTdEJTJDJTIyZXhhbXBsZSUyMiUzQW51bGwlN0Q=
*
* Webpack is not as smart as Rollup, and it usually treat all 'new' expressions as impure, but it
* could be fine to allow it nonetheless at it pulls very little code.
*/
/* eslint-enable max-len */
function isAllowedNewExpression({ callee }) {
// Allow "new WeakMap()"
if (callee.name === 'WeakMap') {
return true
}

return false
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"eslint-plugin-import": "2.22.1",
"eslint-plugin-jasmine": "4.1.2",
"eslint-plugin-jsdoc": "30.7.13",
"eslint-plugin-local-rules": "1.0.1",
"eslint-plugin-prefer-arrow": "1.2.2",
"eslint-plugin-unicorn": "25.0.1",
"express": "4.17.1",
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/domain/automaticErrorCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Observable } from '../tools/observable'
import { jsonStringify, ONE_MINUTE, RequestType } from '../tools/utils'
import { Configuration } from './configuration'
import { monitor } from './internalMonitoring'
import { computeStackTrace, Handler, report, StackTrace } from './tracekit'
import { computeStackTrace, subscribe, unsubscribe, StackTrace } from './tracekit'

export type ErrorObservable = Observable<RawError>
let filteredErrorsObservable: ErrorObservable
Expand Down Expand Up @@ -82,11 +82,11 @@ export function startRuntimeErrorTracking(errorObservable: ErrorObservable) {
startTime: performance.now(),
})
}
;(report.subscribe as (handler: Handler) => void)(traceKitReportHandler)
subscribe(traceKitReportHandler)
}

export function stopRuntimeErrorTracking() {
;(report.unsubscribe as (handler: Handler) => void)(traceKitReportHandler)
unsubscribe(traceKitReportHandler)
}

export function trackNetworkError(configuration: Configuration, errorObservable: ErrorObservable) {
Expand Down
Loading