Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Implement a developer extension #686

Merged
merged 16 commits into from
Feb 17, 2021
Merged
Show file tree
Hide file tree
Changes from 14 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
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
bundle
cjs
esm
dist
test/app/dist
sandbox
coverage
7 changes: 6 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ module.exports = {
],
parser: '@typescript-eslint/parser',
parserOptions: {
project: ['./tsconfig.json', './test/app/tsconfig.json', './test/e2e/tsconfig.json'],
project: [
'./tsconfig.json',
'./test/app/tsconfig.json',
'./test/e2e/tsconfig.json',
'./developer-extension/tsconfig.json',
],
sourceType: 'module',
},
plugins: [
Expand Down
9 changes: 9 additions & 0 deletions LICENSE-3rdparty.csv
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@ file,pako,MIT,(C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
file,rrweb,MIT,Copyright (c) 2018 Contributors (https://github.com/rrweb-io/rrweb/graphs/contributors) and SmartX Inc.
file,rrweb-snapshot,MIT,Copyright (c) 2018 Contributors (https://github.com/rrweb-io/rrweb-snapshot/graphs/contributors) and SmartX Inc.
file,tracekit,MIT,Copyright 2013 Onur Can Cakmak and all TraceKit contributors
prod,bumbag,MIT,Copyright (c) 2020 Bumbag Enterprises
prod,react,MIT,Copyright (c) Facebook, Inc. and its affiliates.
prod,react-dom,MIT,Copyright (c) Facebook, Inc. and its affiliates.
dev,@types/chrome,MIT,Copyright Microsoft Corporation
dev,@types/connect-busboy,MIT,Copyright Microsoft Corporation
dev,@types/cors,MIT,Copyright Microsoft Corporation
dev,@types/express,MIT,Copyright Microsoft Corporation
dev,@types/jasmine,MIT,Copyright Microsoft Corporation
dev,@types/pako,MIT,Copyright Microsoft Corporation
dev,@types/react,MIT,Copyright Microsoft Corporation
dev,@types/react-dom,MIT,Copyright Microsoft Corporation
dev,@types/sinon,MIT,Copyright Microsoft Corporation
dev,@typescript-eslint/eslint-plugin,MIT,Copyright JS Foundation and other contributors
dev,@typescript-eslint/parser,MIT,Copyright JS Foundation and other contributors
Expand All @@ -24,6 +30,7 @@ dev,ajv,MIT,Copyright 2015-2017 Evgeny Poberezkin
dev,browserstack-local,MIT,Copyright 2016 BrowserStack
dev,codecov,MIT,Copyright 2014 Gregg Caines
dev,connect-busboy,MIT,Copyright Brian White
dev,copy-webpack-plugin,MIT,Copyright JS Foundation and other contributors
dev,cors,MIT,Copyright 2013 Troy Goode
dev,emoji-name-map,MIT,Copyright 2016-19 Ionică Bizău <[email protected]> (https://ionicabizau.net)
dev,eslint,MIT,Copyright JS Foundation and other contributors
Expand All @@ -34,6 +41,7 @@ dev,eslint-plugin-jasmine,MIT,Copyright (c) 2021 Tom Vincent
dev,eslint-plugin-prefer-arrow,MIT,Copyright (c) 2018 Triston Jones
dev,eslint-plugin-unicorn,MIT,Copyright (c) Sindre Sorhus <[email protected]> (https://sindresorhus.com)
dev,express,MIT,Copyright 2009-2014 TJ Holowaychuk 2013-2014 Roman Shtylman 2014-2015 Douglas Christopher Wilson
dev,html-webpack-plugin,MIT,Copyright JS Foundation and other contributors
dev,istanbul-instrumenter-loader,MIT,Copyright JS Foundation and other contributors
dev,jasmine-core,MIT,Copyright 2008-2017 Pivotal Labs
dev,js-polyfills,Unlicense,
Expand Down Expand Up @@ -62,3 +70,4 @@ dev,webdriverio,MIT,Copyright JS Foundation and other contributors
dev,webpack,MIT,Copyright JS Foundation and other contributors
dev,webpack-cli,MIT,Copyright JS Foundation and other contributors
dev,webpack-dev-middleware,MIT,Copyright JS Foundation and other contributors
dev,webpack-webextension-plugin,MIT,Copyright 2018 Henrik Wenz ([email protected])
1 change: 1 addition & 0 deletions developer-extension/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/dist/
35 changes: 35 additions & 0 deletions developer-extension/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Browser SDK developer extension

Browser extension to investigate your Browser SDK integration.

## Getting started

The extension is not (yet?) published on addons store. You will need to clone this repository and
build the extension manually.

```
$ git clone https://github.com/DataDog/browser-sdk
$ cd browser-sdk
$ yarn
$ cd developer-extension
$ yarn build
```

Then, in Google Chrome:

- Open the _Extension Management_ page by navigating to [chrome://extensions](chrome://extensions).
- Enable _Developer Mode_ by clicking the toggle switch next to _Developer mode_.
- Click the _LOAD UNPACKED_ button and select the `browser-sdk/developer-extension/dist`
directory.

## Features

- Log events sent by the SDK in the devtools console
- Flush buffered events
- End current session
- Load the SDK development bundles instead of production ones
- Switch between `datadog-rum.js` and `datadog-rum-recorder.js` bundles

## Browser compatibility

For now, only Google Chrome is supported.
Binary file added developer-extension/icons/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions developer-extension/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"manifest_version": 2,
"name": "Datadog Browser SDK developer extension",
"permissions": ["<all_urls>", "tabs", "webRequest", "webRequestBlocking", "storage", "browsingData"],
"icons": {
"256": "icon.png"
},
"background": {
"scripts": ["background.js"],
"persistent": true
},
"browser_action": {
"default_title": "Browser SDK extension",
"default_popup": "popup.html"
}
}
22 changes: 22 additions & 0 deletions developer-extension/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "@datadog/browser-sdk-developer-extension",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "rm -rf dist && webpack --mode production",
"dev": "webpack --mode development --watch"
},
"devDependencies": {
"@types/chrome": "0.0.125",
"@types/react": "16",
"@types/react-dom": "16",
"copy-webpack-plugin": "6",
"html-webpack-plugin": "4.5.1",
"webpack-webextension-plugin": "0.3.0"
},
"dependencies": {
"bumbag": "1.6.12",
"react": "16",
"react-dom": "16"
}
}
5 changes: 5 additions & 0 deletions developer-extension/src/background/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createListenAction, createSendAction } from '../common/actions'
import { BackgroundActions, PopupActions } from '../common/types'

export const listenAction = createListenAction<BackgroundActions>()
export const sendAction = createSendAction<PopupActions>()
3 changes: 3 additions & 0 deletions developer-extension/src/background/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const DEV_LOGS_URL = 'http://localhost:8080/datadog-logs.js'
export const DEV_RUM_RECORDER_URL = 'http://localhost:8080/datadog-rum-recorder.js'
export const DEV_RUM_URL = 'http://localhost:8080/datadog-rum.js'
9 changes: 9 additions & 0 deletions developer-extension/src/background/domain/endSession.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { listenAction } from '../actions'
import { evaluateCodeInActiveTab } from '../utils'

listenAction('endSession', () => {
evaluateCodeInActiveTab(() => {
console.log('plop')
document.cookie = '_dd_s=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'
})
})
13 changes: 13 additions & 0 deletions developer-extension/src/background/domain/flushEvents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { listenAction } from '../actions'
import { evaluateCodeInActiveTab } from '../utils'

listenAction('flushEvents', () =>
// Simulates a brief page visibility change (visible > hide > visible)
evaluateCodeInActiveTab(() => {
const descriptor = Object.getOwnPropertyDescriptor(Document.prototype, 'visibilityState')
Object.defineProperty(Document.prototype, 'visibilityState', { value: 'hidden' })
document.dispatchEvent(new Event('visibilitychange', { bubbles: true }))
Object.defineProperty(Document.prototype, 'visibilityState', descriptor)
document.dispatchEvent(new Event('visibilitychange', { bubbles: true }))
})
)
74 changes: 74 additions & 0 deletions developer-extension/src/background/domain/logEventsFromRequests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { store } from '../store'

const decoder = new TextDecoder('utf-8')
chrome.webRequest.onBeforeRequest.addListener(
(info) => {
if (!store.logEventsFromRequests) {
return
}
if (info.tabId < 0) {
console.log('Some intake request was made in a non-tab context... (service worker maybe?)')
return
}

const url = new URL(info.url)

const intake = /^\w*/.exec(url.hostname)?.[0]
if (!intake) {
return
}
if (!info.requestBody.raw) {
return
}

for (const rawBody of info.requestBody.raw) {
if (rawBody.bytes) {
const decodedBody = decoder.decode(rawBody.bytes)
for (const rawEvent of decodedBody.split('\n')) {
const event = sortProperties(JSON.parse(rawEvent))
chrome.tabs.executeScript(info.tabId, {
code: `console.info("Browser-SDK:", ${JSON.stringify(intake)}, ${JSON.stringify(event)});`,
})
}
}
}
},
{
urls: [
// TODO: implement a configuration page to add more URLs in this list.
'https://*.browser-intake-datadoghq.com/*',
'https://*.browser-intake-datadoghq.eu/*',
'https://*.browser-intake-ddog-gov.com/*',
'https://*.browser-intake-us3-datadoghq.com/*',
...classicIntakesUrlsForSite('datadoghq.com'),
...classicIntakesUrlsForSite('datadoghq.eu'),
],
},
['requestBody']
)

function classicIntakesUrlsForSite(site: string) {
return [
`https://public-trace-http-intake.logs.${site}/*`,
`https://rum-http-intake.logs.${site}/*`,
`https://browser-http-intake.logs.${site}/*`,
]
}

function sortProperties<T extends unknown>(event: T): T {
if (Array.isArray(event)) {
return event.map(sortProperties) as T
}

if (typeof event === 'object' && event !== null) {
const names = Object.getOwnPropertyNames(event)
names.sort()
const result: { [key: string]: unknown } = {}
names.forEach((name) => {
result[name] = sortProperties((event as { [key: string]: unknown })[name])
})
return result as T
}

return event
}
61 changes: 61 additions & 0 deletions developer-extension/src/background/domain/replaceBundles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { listenAction } from '../actions'
import { DEV_LOGS_URL, DEV_RUM_RECORDER_URL, DEV_RUM_URL } from '../constants'
import { setStore, store } from '../store'

chrome.webRequest.onBeforeRequest.addListener(
(info) => {
if (store.useDevBundles) {
const url = new URL(info.url)
if (url.pathname.includes('logs')) {
return { redirectUrl: DEV_LOGS_URL }
}
if (url.pathname.includes('rum')) {
return {
redirectUrl: store.useRumRecorder ? DEV_RUM_RECORDER_URL : DEV_RUM_URL,
}
}
} else if (store.useRumRecorder && /\/datadog-rum(?!-recorder)\.js$/.test(info.url)) {
return {
redirectUrl: info.url.replace(/\.js$/, '-recorder.js'),
}
}
return
},
{
types: ['script'],
urls: [
// TODO: implement a configuration page to add more URLs in this list.
'https://www.datadoghq-browser-agent.com/datadog-logs.js',
'https://www.datadoghq-browser-agent.com/datadog-rum.js',
'https://www.datadoghq-browser-agent.com/datadog-rum-recorder.js',
'https://localhost:8443/static/datadog-rum-hotdog.js',
],
},
['blocking']
)

listenAction('getStore', () => {
refreshDevServerStatus().catch((error) =>
console.error('Unexpected error while refreshing dev server status:', error)
)
})

listenAction('setStore', (newStore) => {
if ('useDevBundles' in newStore || 'useRumRecorder' in newStore) {
chrome.browsingData.removeCache({})
}
})

async function refreshDevServerStatus() {
const timeoutId = setTimeout(() => setStore({ devServerStatus: 'checking' }), 500)
let isAvailable = false
try {
const response = await fetch(DEV_LOGS_URL, { method: 'HEAD' })
isAvailable = response.status === 200
} catch {
// The request can fail if nothing is listening on the URL port. In this case, consider the dev
// server 'unavailable'.
}
clearTimeout(timeoutId)
setStore({ devServerStatus: isAvailable ? 'available' : 'unavailable' })
}
4 changes: 4 additions & 0 deletions developer-extension/src/background/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import './domain/flushEvents'
import './domain/logEventsFromRequests'
import './domain/endSession'
import './domain/replaceBundles'
30 changes: 30 additions & 0 deletions developer-extension/src/background/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Store } from '../common/types'
import { listenAction, sendAction } from './actions'

export const store: Store = {
devServerStatus: 'checking',
logEventsFromRequests: true,
useDevBundles: false,
useRumRecorder: false,
}

export function setStore(newStore: Partial<Store>) {
if (wouldModifyStore(newStore)) {
Object.assign(store, newStore)
sendAction('newStore', store)
chrome.storage.local.set({ store })
}
}

listenAction('getStore', () => sendAction('newStore', store))
listenAction('setStore', (newStore) => setStore(newStore))

chrome.storage.local.get((storage) => {
if (storage.store) {
setStore(storage.store as Store)
}
})

function wouldModifyStore(newStore: Partial<Store>) {
return (Object.entries(newStore) as Array<[keyof Store, unknown]>).some(([key, value]) => store[key] !== value)
}
21 changes: 21 additions & 0 deletions developer-extension/src/background/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export function evaluateCodeInActiveTab(code: () => void) {
chrome.tabs.query({ currentWindow: true, active: true }, (tabs) => {
for (const tab of tabs) {
if (tab.id) {
evaluateCodeInline(tab.id, code)
}
}
})
}

function evaluateCodeInline(tabId: number, code: () => void) {
chrome.tabs.executeScript(tabId, {
code: `{
const script = document.createElement('script')
script.setAttribute("type", "module")
script.textContent = ${JSON.stringify(`(${String(code)})()`)}
document.body.appendChild(script)
script.remove()
}`,
})
}
Loading