From e46b7001f7ebfb34f7f01fd732e0c7650cd90bbc Mon Sep 17 00:00:00 2001 From: Evan Purkhiser Date: Tue, 20 Oct 2020 01:38:55 -0700 Subject: [PATCH] app: Implement service object for recording all events for debugging --- package.json | 7 +- src/main/debugEvents.ts | 160 ++++++++++++++++++++++++++++++++++++ src/main/main.ts | 2 + src/overlay/index.tsx | 1 + src/shared/store/index.ts | 6 ++ src/shared/store/network.ts | 11 ++- yarn.lock | 39 +++++++-- 7 files changed, 214 insertions(+), 12 deletions(-) create mode 100644 src/main/debugEvents.ts diff --git a/package.json b/package.json index aac96cf..bdc8856 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,8 @@ "@types/lodash": "^4.14.161", "@types/module-alias": "^2.0.0", "@types/node": "14.6.4", + "@types/node-fetch": "^2.5.7", + "@types/node-gzip": "^1.1.0", "@types/node-static": "^0.7.5", "@types/object-path": "^0.11.0", "@types/react": "^16.9.49", @@ -103,6 +105,7 @@ "file-loader": "^6.0.0", "filesize": "^6.1.0", "fork-ts-checker-webpack-plugin": "^5.1.0", + "form-data": "^3.0.0", "framer-motion": "^1.11.1", "html-webpack-plugin": "^4.3.0", "http-proxy": "^1.18.1", @@ -113,9 +116,11 @@ "mobx-react": "^6.2.2", "mobx-utils": "^5.5.7", "module-alias": "^2.2.2", + "node-fetch": "^2.6.1", + "node-gzip": "^1.1.2", "node-static": "^0.7.11", "noop2": "^2.0.0", - "prolink-connect": "0.2.0-prerelease.11", + "prolink-connect": "0.2.0-prerelease.12", "public-ip": "^4.0.1", "react": "^16.8.6", "react-dom": "^16.8.6", diff --git a/src/main/debugEvents.ts b/src/main/debugEvents.ts new file mode 100644 index 0000000..da20cfb --- /dev/null +++ b/src/main/debugEvents.ts @@ -0,0 +1,160 @@ +import * as Sentry from '@sentry/node'; +import fetch from 'node-fetch'; +import FormData from 'form-data'; +import { + CDJStatus, + ConnectedProlinkNetwork, + Device, + ProlinkNetwork, +} from 'prolink-connect'; +import {autorun} from 'mobx'; +import {gzip} from 'node-gzip'; + +import store from 'src/shared/store'; + +type Events = + | { + type: 'status'; + event: CDJStatus.State; + } + | { + type: 'nowPlaying'; + event: CDJStatus.State; + } + | { + type: 'deviceAdded'; + event: Device; + } + | { + type: 'deviceRemoved'; + event: Device; + }; + +type DebugEvent = {ts: number} & Events; + +async function uploadEvents(eventId: string, data: Buffer) { + const dsn = Sentry.getCurrentHub().getClient()?.getDsn(); + + if (dsn === undefined) { + return; + } + + // Determine the sentry attachment upload URL + const {host, path, projectId, port, protocol, user} = dsn; + const portString = port !== '' ? `:${port}` : ''; + const pathString = path !== '' ? `/${path}` : ''; + + const endpoint = `${protocol}://${host}${portString}${pathString}/api/${projectId}/events/${eventId}/attachments/?sentry_key=${user}&sentry_version=7&sentry_client=custom-javascript`; + + // Transform our event history into uploadable FormData + const formData = new FormData(); + + formData.append('my-attachment', await gzip(data), { + filename: 'events.json.gz', + contentType: 'application/json', + }); + + // Upload debug events blob to Sentry + await fetch(endpoint, {method: 'POST', body: formData}); +} + +class DebugEventsService { + #network: ConnectedProlinkNetwork; + /** + * Is the service currently recording and reporting? + */ + #active = false; + /** + * Records events for the current DJ set + */ + #eventHistory: DebugEvent[] = []; + + constructor(network: ConnectedProlinkNetwork) { + this.#network = network; + } + + toggle(enabled: boolean) { + return enabled ? this.enable() : this.disable(); + } + + /** + * Start capturing events for debugging + */ + enable() { + if (this.#active) { + return; + } + + this.#network.statusEmitter.on('status', this.#handleStatus); + this.#network.mixstatus.on('setStarted', this.#handleSetStarted); + this.#network.mixstatus.on('setEnded', this.#handleSetEnded); + this.#network.mixstatus.on('nowPlaying', this.#handleNowPlaying); + this.#network.deviceManager.on('connected', this.#handleNewDevice); + this.#network.deviceManager.on('disconnected', this.#handleRemovedDevice); + + this.#active = true; + } + + /** + * Stop capturing events for debugging + */ + disable() { + if (!this.#active) { + return; + } + + this.#network.statusEmitter.off('status', this.#handleStatus); + this.#network.mixstatus.off('setStarted', this.#handleSetStarted); + this.#network.mixstatus.off('setEnded', this.#handleSetEnded); + this.#network.mixstatus.off('nowPlaying', this.#handleNowPlaying); + this.#network.deviceManager.off('connected', this.#handleNewDevice); + this.#network.deviceManager.off('disconnected', this.#handleRemovedDevice); + + this.#eventHistory = []; + this.#active = false; + } + + #handleNewDevice = (event: Device) => { + this.#eventHistory.push({ts: Date.now(), type: 'deviceAdded', event}); + }; + + #handleRemovedDevice = (event: Device) => { + this.#eventHistory.push({ts: Date.now(), type: 'deviceRemoved', event}); + }; + + #handleNowPlaying = (event: CDJStatus.State) => { + this.#eventHistory.push({ts: Date.now(), type: 'nowPlaying', event}); + }; + + #handleStatus = (event: CDJStatus.State) => { + this.#eventHistory.push({ts: Date.now(), type: 'status', event}); + }; + + // Control events. These events will control the lifecycle of capturing debug + // events. Including uploading the events list to Sentry. + + #handleSetStarted = () => { + this.#eventHistory = []; + }; + + #handleSetEnded = () => { + const eventId = Sentry.captureMessage('Debug events uploaded', Sentry.Severity.Debug); + const data = Buffer.from(JSON.stringify(this.#eventHistory)); + + uploadEvents(eventId, data); + }; +} + +/** + * Registers availability to capture and upload debug events from the network + * to Sentry. The service will be activated and deactivated reactively to the + * reportDebugEvents store configuration. + */ +export function registerDebuggingEventsService(network: ProlinkNetwork) { + if (!network.isConnected()) { + return; + } + + const service = new DebugEventsService(network); + autorun(() => service.toggle(store.config.reportDebugEvents)); +} diff --git a/src/main/main.ts b/src/main/main.ts index b802b5e..db1298e 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -11,6 +11,7 @@ import {startOverlayServer} from 'main/overlayServer'; import {registerMainIpc, observeStore, loadMainConfig} from 'src/shared/store/ipc'; import connectNetworkStore from 'src/shared/store/network'; import store from 'src/shared/store'; +import {registerDebuggingEventsService} from './debugEvents'; // Intialize the store for the main thread immedaitely. store.isInitalized = true; @@ -99,6 +100,7 @@ app.on('ready', async () => { await startOverlayServer(); connectNetworkStore(network); + registerDebuggingEventsService(network); }); app.on('window-all-closed', () => { diff --git a/src/overlay/index.tsx b/src/overlay/index.tsx index b02a9b5..3afe6f1 100644 --- a/src/overlay/index.tsx +++ b/src/overlay/index.tsx @@ -1,4 +1,5 @@ import taggedNowPlaying from './overlays/taggedNowPlaying'; +import classicMetadata from './overlays/classicMetadata'; type OverlayType = typeof registeredOverlays[number]['type']; diff --git a/src/shared/store/index.ts b/src/shared/store/index.ts index 418461b..d73937a 100644 --- a/src/shared/store/index.ts +++ b/src/shared/store/index.ts @@ -142,6 +142,12 @@ export class AppConfig { @serializable(list(rawJS)) @observable overlays = observable.array(); + /** + * Should debug events be enabled to be stored and uploaded? + */ + @serializable + @observable + reportDebugEvents = false; } export class AppStore { diff --git a/src/shared/store/network.ts b/src/shared/store/network.ts index 97a714e..9526efc 100644 --- a/src/shared/store/network.ts +++ b/src/shared/store/network.ts @@ -231,11 +231,11 @@ const connectLocaldbHydrateDone = (network: ConnectedProlinkNetwork) => }) ); -const connectMixstatus = (network: ConnectedProlinkNetwork) => { - const mixstatus = new MixstatusProcessor(); - network.statusEmitter.on('status', s => mixstatus.handleState(s)); - - mixstatus.on( +/** + * Configure listeners for the mixstatus processor + */ +const connectMixstatus = (network: ConnectedProlinkNetwork) => + network.mixstatus.on( 'nowPlaying', action(async state => { const playedAt = new Date(); @@ -268,4 +268,3 @@ const connectMixstatus = (network: ConnectedProlinkNetwork) => { store.mixstatus.trackHistory.push(played); }) ); -}; diff --git a/yarn.lock b/yarn.lock index abe598d..2ed6d45 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1796,6 +1796,21 @@ resolved "https://registry.yarnpkg.com/@types/module-alias/-/module-alias-2.0.0.tgz#882668f8b8cdbda44812c3b592c590909e18849e" integrity sha512-e3sW4oEH0qS1QxSfX7PT6xIi5qk/YSMsrB9Lq8EtkhQBZB+bKyfkP+jpLJRySanvBhAQPSv2PEBe81M8Iy/7yg== +"@types/node-fetch@^2.5.7": + version "2.5.7" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.7.tgz#20a2afffa882ab04d44ca786449a276f9f6bbf3c" + integrity sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw== + dependencies: + "@types/node" "*" + form-data "^3.0.0" + +"@types/node-gzip@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@types/node-gzip/-/node-gzip-1.1.0.tgz#99a7dfab7c0eec545658f3d736e8d6939ed7161e" + integrity sha512-j7cGb6HIOZbDx3sqe9/9VAPeSvyt143yu5k35gzRXE3mxEgK6BOZ6BAiJ3ToXBcJqLzL9Cr53dav21jlp3f9gw== + dependencies: + "@types/node" "*" + "@types/node-static@^0.7.5": version "0.7.5" resolved "https://registry.yarnpkg.com/@types/node-static/-/node-static-0.7.5.tgz#9ebf105804b5b2aab896b783718ebf1a145d27b4" @@ -3686,7 +3701,7 @@ colors@>=0.6.0: resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== -combined-stream@^1.0.6, combined-stream@~1.0.6: +combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -5622,6 +5637,15 @@ fork-ts-checker-webpack-plugin@^5.1.0: semver "^7.3.2" tapable "^1.0.0" +form-data@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682" + integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -8646,6 +8670,11 @@ node-forge@^0.10.0: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== +node-gzip@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/node-gzip/-/node-gzip-1.1.2.tgz#245bd171b31ce7c7f50fc4cd0ca7195534359afb" + integrity sha512-ZB6zWpfZHGtxZnPMrJSKHVPrRjURoUzaDbLFj3VO70mpLTW5np96vXyHwft4Id0o+PYIzgDkBUjIzaNHhQ8srw== + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -9564,10 +9593,10 @@ progress@^2.0.1, progress@^2.0.3: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== -prolink-connect@0.2.0-prerelease.11: - version "0.2.0-prerelease.11" - resolved "https://registry.yarnpkg.com/prolink-connect/-/prolink-connect-0.2.0-prerelease.11.tgz#049574eb7a085fd19de2ad095a09926c0a7eac6e" - integrity sha512-6+eYAJ0ht6ahDOBkyM7lJ4UAUcbjiqxNVKLNAUK36Pic6CM7072/3DF14WrRRZb9FCRke+rkEnmVZvMOCyn7/w== +prolink-connect@0.2.0-prerelease.12: + version "0.2.0-prerelease.12" + resolved "https://registry.yarnpkg.com/prolink-connect/-/prolink-connect-0.2.0-prerelease.12.tgz#7d5f9a42d8a005dccc7360071810be5ca8f01c8e" + integrity sha512-NpPrmJ6Ocx7iqgfmQstAD9E5u/vGv1HPqXi0Eug4bdw6S1uPZVa0MN/To4LkpiuP6htrf9NZgcJEVwtfXxYdgw== dependencies: "@sentry/node" "^5.16.1" "@sentry/tracing" "^5.25.0"