Skip to content

Commit

Permalink
refactor: ♻️ refactor plugin-inline-script-content
Browse files Browse the repository at this point in the history
  • Loading branch information
gingerbenw committed Dec 5, 2024
1 parent a64d54b commit 08a3dc9
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 23 deletions.
16 changes: 14 additions & 2 deletions packages/plugin-inline-script-content/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
{
"name": "@bugsnag/plugin-inline-script-content",
"version": "8.1.1",
"main": "inline-script-content.js",
"main": "dist/inline-script-content.js",
"types": "dist/types/inline-script-content.d.ts",
"exports": {
".": {
"types": "./dist/types/inline-script-content.d.ts",
"default": "./dist/inline-script-content.js",
"import": "./dist/inline-script-content.mjs"
}
},
"description": "@bugsnag/js plugin to attach inline script content to error events",
"homepage": "https://www.bugsnag.com/",
"repository": {
Expand All @@ -14,7 +22,11 @@
"files": [
"*.js"
],
"scripts": {},
"scripts": {
"build": "npm run build:npm",
"build:npm": "rollup --config rollup.config.npm.mjs",
"clean": "rm -rf dist/*"
},
"author": "Bugsnag",
"license": "MIT",
"devDependencies": {
Expand Down
6 changes: 6 additions & 0 deletions packages/plugin-inline-script-content/rollup.config.npm.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import createRollupConfig from '../../.rollup/index.mjs'

export default createRollupConfig({
input: 'src/inline-script-content.ts',
external: ['@bugsnag/core/lib/es-utils/map', '@bugsnag/core/lib/es-utils/reduce', '@bugsnag/core/lib/es-utils/filter']
})
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
const map = require('@bugsnag/core/lib/es-utils/map')
const reduce = require('@bugsnag/core/lib/es-utils/reduce')
const filter = require('@bugsnag/core/lib/es-utils/filter')
import map from '@bugsnag/core/lib/es-utils/map'
import reduce from '@bugsnag/core/lib/es-utils/reduce'
import filter from '@bugsnag/core/lib/es-utils/filter'
import type { Config, Plugin } from '@bugsnag/core/types'
import type ClientWithInternals from 'packages/core/client'

const MAX_LINE_LENGTH = 200
const MAX_SCRIPT_LENGTH = 500000

module.exports = (doc = document, win = window) => ({
interface ExtendedConfig extends Config {
trackInlineScripts: boolean
}

interface ExtendedDocument extends Document {
attachEvent?: unknown
}

interface ValidationOption {
validate: (value: unknown) => boolean
defaultValue: () => unknown
message: string
}

interface ExtendedPlugin extends Plugin {
configSchema: Record<string, ValidationOption>
}

export default (doc: ExtendedDocument = document, win = window): ExtendedPlugin => ({
load: (client) => {
if (!client._config.trackInlineScripts) return
if (!(client as ClientWithInternals<ExtendedConfig>)._config.trackInlineScripts) return

const originalLocation = win.location.href
let html = ''
Expand All @@ -28,15 +48,15 @@ module.exports = (doc = document, win = window) => ({
html = getHtml()
DOMContentLoaded = true
}
try { prev.apply(this, arguments) } catch (e) {}
try { prev && prev.apply(this, arguments as unknown as Parameters<typeof prev>) } catch (e) {}
}

let _lastScript = null
const updateLastScript = script => {
let _lastScript: HTMLOrSVGScriptElement | null = null
const updateLastScript = (script: HTMLOrSVGScriptElement | null) => {
_lastScript = script
}

const getCurrentScript = () => {
const getCurrentScript = (): HTMLOrSVGScriptElement | null => {
let script = doc.currentScript || _lastScript
if (!script && !DOMContentLoaded) {
const scripts = doc.scripts || doc.getElementsByTagName('script')
Expand All @@ -45,7 +65,7 @@ module.exports = (doc = document, win = window) => ({
return script
}

const addSurroundingCode = lineNumber => {
const addSurroundingCode = (lineNumber: number) => {
// get whatever html has rendered at this point
if (!DOMContentLoaded || !html) html = getHtml()
// simulate the raw html
Expand All @@ -62,12 +82,12 @@ module.exports = (doc = document, win = window) => ({
client.addOnError(event => {
// remove any of our own frames that may be part the stack this
// happens before the inline script check as it happens for all errors
event.errors[0].stacktrace = filter(event.errors[0].stacktrace, f => !(/__trace__$/.test(f.method)))
event.errors[0].stacktrace = filter(event.errors[0].stacktrace, f => !(/__trace__$/.test(String(f.method))))

const frame = event.errors[0].stacktrace[0]

// remove hash and query string from url
const cleanUrl = (url) => url.replace(/#.*$/, '').replace(/\?.*$/, '')
const cleanUrl = (url: string) => url.replace(/#.*$/, '').replace(/\?.*$/, '')

// if frame.file exists and is not the original location of the page, this can't be an inline script
if (frame && frame.file && cleanUrl(frame.file) !== cleanUrl(originalLocation)) return
Expand All @@ -87,6 +107,7 @@ module.exports = (doc = document, win = window) => ({
frame.code = addSurroundingCode(frame.lineNumber)
}
}
// @ts-expect-error second argument is private API
}, true)

// Proxy all the timer functions whose callback is their 0th argument.
Expand All @@ -105,14 +126,18 @@ module.exports = (doc = document, win = window) => ({
)
)

type ValidWindowProperties = 'EventTarget' | 'Window' | 'Node' | 'ChannelMergerNode' | 'EventSource' | 'FileReader' | 'HTMLUnknownElement' | 'IDBDatabase' | 'IDBRequest' | 'IDBTransaction' | 'MessagePort' | 'Notification' | 'Screen' | 'TextTrack' | 'TextTrackCue' | 'TextTrackList' | 'WebSocket' | 'Worker' | 'XMLHttpRequest' | 'XMLHttpRequestEventTarget' | 'XMLHttpRequestUpload'

type WindowProperties = keyof Pick<Window & typeof globalThis, ValidWindowProperties>

// Proxy all the host objects whose prototypes have an addEventListener function
map([
'EventTarget', 'Window', 'Node', 'ApplicationCache', 'AudioTrackList', 'ChannelMergerNode',
'CryptoOperation', 'EventSource', 'FileReader', 'HTMLUnknownElement', 'IDBDatabase',
'IDBRequest', 'IDBTransaction', 'KeyOperation', 'MediaController', 'MessagePort', 'ModalWindow',
'Notification', 'SVGElementInstance', 'Screen', 'TextTrack', 'TextTrackCue', 'TextTrackList',
'WebSocket', 'WebSocketWorker', 'Worker', 'XMLHttpRequest', 'XMLHttpRequestEventTarget', 'XMLHttpRequestUpload'
], o => {
] as WindowProperties[], o => {
if (!win[o] || !win[o].prototype || !Object.prototype.hasOwnProperty.call(win[o].prototype, 'addEventListener')) return
__proxy(win[o].prototype, 'addEventListener', original =>
__traceOriginalScript(original, eventTargetCallbackAccessor)
Expand All @@ -122,16 +147,16 @@ module.exports = (doc = document, win = window) => ({
)
})

function __traceOriginalScript (fn, callbackAccessor, alsoCallOriginal = false) {
function __traceOriginalScript (fn: Function, callbackAccessor: EventTargetCallbackAccessor, alsoCallOriginal = false) {
return function () {
// this is required for removeEventListener to remove anything added with
// addEventListener before the functions started being wrapped by Bugsnag
const args = [].slice.call(arguments)
try {
const cba = callbackAccessor(args)
const cb = cba.get()
if (alsoCallOriginal) fn.apply(this, args)
if (typeof cb !== 'function') return fn.apply(this, args)
if (alsoCallOriginal) fn.apply(fn, args)
if (typeof cb !== 'function') return fn.apply(fn, args)
if (cb.__trace__) {
cba.replace(cb.__trace__)
} else {
Expand Down Expand Up @@ -159,7 +184,7 @@ module.exports = (doc = document, win = window) => ({
// WebDriverException: Message: Permission denied to access property "handleEvent"
}
// IE8 doesn't let you call .apply() on setTimeout/setInterval
if (fn.apply) return fn.apply(this, args)
if (fn.apply) return fn.apply(fn, args)
switch (args.length) {
case 1: return fn(args[0])
case 2: return fn(args[0], args[1])
Expand All @@ -177,21 +202,27 @@ module.exports = (doc = document, win = window) => ({
}
})

function __proxy (host, name, replacer) {
function __proxy (host: any, name: string, replacer: (original: Function) => Function) {
const original = host[name]
if (!original) return original
const replacement = replacer(original)
host[name] = replacement
return original
}

function eventTargetCallbackAccessor (args) {
type NestedFunction = Function & { __trace__?: NestedFunction }

type Argument = NestedFunction & {
handleEvent?: NestedFunction
}

function eventTargetCallbackAccessor (args: Argument[]) {
const isEventHandlerObj = !!args[1] && typeof args[1].handleEvent === 'function'
return {
get: function () {
return isEventHandlerObj ? args[1].handleEvent : args[1]
},
replace: function (fn) {
replace: function (fn: Function) {
if (isEventHandlerObj) {
args[1].handleEvent = fn
} else {
Expand All @@ -200,3 +231,5 @@ function eventTargetCallbackAccessor (args) {
}
}
}

type EventTargetCallbackAccessor = typeof eventTargetCallbackAccessor
5 changes: 5 additions & 0 deletions packages/plugin-inline-script-content/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.json",
"include": ["src/**/*.ts"]
}

2 changes: 1 addition & 1 deletion packages/plugin-interaction-breadcrumbs/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@bugsnag/plugin-interaction-breadcrumbs",
"version": "8.1.1",
"main": "interaction-breadcrumbs.js",
"main": "dist/interaction-breadcrumbs.js",
"types": "dist/types/interaction-breadcrumbs.d.ts",
"exports": {
".": {
Expand Down

0 comments on commit 08a3dc9

Please sign in to comment.