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 1 commit
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
9 changes: 9 additions & 0 deletions LICENSE-3rdparty.csv
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ require,tslib,Apache-2.0,Copyright Microsoft Corporation
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,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.
prod,rrweb-snapshot,MIT,Copyright (c) 2018 Contributors (https://github.com/rrweb-io/rrweb-snapshot/graphs/contributors) and SmartX Inc.
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/react,MIT,Copyright Microsoft Corporation
dev,@types/react-dom,MIT,Copyright Microsoft Corporation
dev,@types/sinon,MIT,Copyright Microsoft Corporation
dev,@wdio/browserstack-service,MIT,Copyright JS Foundation and other contributors
dev,@wdio/cli,MIT,Copyright JS Foundation and other contributors
Expand All @@ -21,9 +27,11 @@ 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 <bizauionica@gmail.com> (https://ionicabizau.net)
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 @@ -56,3 +64,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 (handtrix@gmail.com)
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"build:bundle": "lerna run build:bundle --stream",
"format": "prettier --check .",
"lint": "scripts/cli lint . && scripts/cli lint test/e2e",
"typecheck": "scripts/cli typecheck . && scripts/cli typecheck test/e2e",
"typecheck": "scripts/cli typecheck",
"dev": "node scripts/dev-server.js",
"release": "lerna version --exact",
"version": "node ./scripts/generate-changelog.js",
Expand Down
1 change: 1 addition & 0 deletions packages/developer-extension/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/dist/
35 changes: 35 additions & 0 deletions packages/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 packages/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/packages/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 packages/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 packages/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"],
"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 packages/developer-extension/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
BenoitZugmeyer marked this conversation as resolved.
Show resolved Hide resolved
"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 packages/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 packages/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'
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { listenAction } from '../actions'
import { evaluateCodeInActiveTab } from '../utils'

listenAction('endSession', () => {
evaluateCodeInActiveTab(`
document.cookie = '_dd_s=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'
`)
})
13 changes: 13 additions & 0 deletions packages/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 }))
BenoitZugmeyer marked this conversation as resolved.
Show resolved Hide resolved
`)
)
64 changes: 64 additions & 0 deletions packages/developer-extension/src/background/domain/logRequests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { store } from '../store'

const decoder = new TextDecoder('utf-8')
chrome.webRequest.onBeforeRequest.addListener(
(info) => {
if (!store.logEvents) {
BenoitZugmeyer marked this conversation as resolved.
Show resolved Hide resolved
bcaudan marked this conversation as resolved.
Show resolved Hide resolved
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 = url.hostname.match(/^\w*/)?.[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://*.logs.datadoghq.com/*',
'https://*.browser-intake-datadoghq.com/*',
'https://*.logs.datadoghq.eu/*',
'https://*.browser-intake-datadoghq.eu/*',
BenoitZugmeyer marked this conversation as resolved.
Show resolved Hide resolved
BenoitZugmeyer marked this conversation as resolved.
Show resolved Hide resolved
],
},
['requestBody']
)

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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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(
BenoitZugmeyer marked this conversation as resolved.
Show resolved Hide resolved
(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',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the SDK is configured to use a different intake domain? Is it possible generate these URLs based on the actual RUM and logs configurations?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This list is not intake domains, but I agree it would be great to be able to configure bundle URLs and intake lists. I'll add a list of feature ideas somewhere.

I'm not sure how to discover such URLs based on page the RUM and Logs configurations though.

'https://localhost:8443/static/datadog-rum-hotdog.js',
],
},
['blocking']
)

listenAction('getStore', () => refreshDevServerStatus())

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 packages/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/logRequests'
import './domain/endSession'
import './domain/replaceBundles'
30 changes: 30 additions & 0 deletions packages/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',
logEvents: 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 })
bcaudan marked this conversation as resolved.
Show resolved Hide resolved
}
}

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 packages/developer-extension/src/background/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export function evaluateCodeInActiveTab(code: string) {
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: string) {
chrome.tabs.executeScript(tabId, {
code: `{
const script = document.createElement('script')
script.setAttribute("type", "module")
script.textContent = ${JSON.stringify(code)}
document.body.appendChild(script)
script.remove()
}`,
})
}
43 changes: 43 additions & 0 deletions packages/developer-extension/src/common/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
type ValueOf<T> = T[keyof T]

type Message<Actions extends { [key: string]: any }> = ValueOf<
{
[K in keyof Actions]: {
action: K
payload: Actions[K]
}
}
>

export function createListenAction<Actions>() {
function listenAction<K extends keyof Actions>(action: K, callback: (payload: Actions[K]) => void) {
const listener = (message: Message<Actions>) => {
if (message.action === action) {
callback((message as Message<Pick<Actions, K>>).payload)
}
}
chrome.runtime.onMessage.addListener(listener)
return () => {
chrome.runtime.onMessage.removeListener(listener)
}
}

return listenAction
}

export function createSendAction<Actions>() {
function sendAction<K extends keyof Actions>(action: K, payload: Actions[K]) {
return chrome.runtime.sendMessage({ action, payload }, () => {
const error = chrome.runtime.lastError
if (
error &&
error.message !== 'Could not establish connection. Receiving end does not exist.' &&
error.message !== 'The message port closed before a response was received.'
) {
console.error(`sendAction error: ${error.message}`)
}
})
}

return sendAction
}
17 changes: 17 additions & 0 deletions packages/developer-extension/src/common/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export interface BackgroundActions {
getStore: void
setStore: Partial<Store>
flushEvents: void
endSession: void
}

export interface PopupActions {
newStore: Store
}

export interface Store {
devServerStatus: 'unavailable' | 'checking' | 'available'
useDevBundles: boolean
useRumRecorder: boolean
logEvents: boolean
}
Loading