├── .github ├── no-response.yml └── workflows │ └── ci.yml ├── .gitignore ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE.md ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── docs └── events.md ├── lib ├── metrics.js ├── reporter.js └── repository-helpers.js ├── package.json └── spec ├── fixtures ├── keymaps │ ├── custom-keymap.cson │ └── default-keymap.cson └── packages │ └── example │ └── lib │ └── example.js ├── helpers └── async-spec-helpers.js ├── metrics-spec.js └── repository-helpers-spec.js /.github/no-response.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-no-response - https://github.com/probot/no-response 2 | 3 | # Number of days of inactivity before an issue is closed for lack of response 4 | daysUntilClose: 28 5 | 6 | # Label requiring a response 7 | responseRequiredLabel: more-information-needed 8 | 9 | # Comment to post when closing an issue for lack of response. Set to `false` to disable. 10 | closeComment: > 11 | This issue has been automatically closed because there has been no response 12 | to our request for more information from the original author. With only the 13 | information that is currently in the issue, we don't have enough information 14 | to take action. Please reach out if you have or find the answers we need so 15 | that we can investigate further. 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | env: 6 | CI: true 7 | 8 | jobs: 9 | Test: 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest, macos-latest, windows-latest] 13 | channel: [stable, beta] 14 | runs-on: ${{ matrix.os }} 15 | steps: 16 | - uses: actions/checkout@v1 17 | - uses: UziTech/action-setup-atom@v2 18 | with: 19 | version: ${{ matrix.channel }} 20 | - name: Install dependencies 21 | run: apm install 22 | - name: Run tests 23 | run: atom --test spec 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | See the [Atom contributing guide](https://github.com/atom/atom/blob/master/CONTRIBUTING.md) 2 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ### Prerequisites 10 | 11 | * [ ] Put an X between the brackets on this line if you have done all of the following: 12 | * Reproduced the problem in Safe Mode: http://flight-manual.atom.io/hacking-atom/sections/debugging/#using-safe-mode 13 | * Followed all applicable steps in the debugging guide: http://flight-manual.atom.io/hacking-atom/sections/debugging/ 14 | * Checked the FAQs on the message board for common solutions: https://discuss.atom.io/c/faq 15 | * Checked that your issue isn't already filed: https://github.com/issues?utf8=✓&q=is%3Aissue+user%3Aatom 16 | * Checked that there is not already an Atom package that provides the described functionality: https://atom.io/packages 17 | 18 | ### Description 19 | 20 | [Description of the issue] 21 | 22 | ### Steps to Reproduce 23 | 24 | 1. [First Step] 25 | 2. [Second Step] 26 | 3. [and so on...] 27 | 28 | **Expected behavior:** [What you expect to happen] 29 | 30 | **Actual behavior:** [What actually happens] 31 | 32 | **Reproduces how often:** [What percentage of the time does it reproduce?] 33 | 34 | ### Versions 35 | 36 | You can get this information from copy and pasting the output of `atom --version` and `apm --version` from the command line. Also, please include the OS and what version of the OS you're running. 37 | 38 | ### Additional Information 39 | 40 | Any additional information, configuration or data that might be necessary to reproduce the issue. 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 GitHub Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Requirements 2 | 3 | * Filling out the template is required. Any pull request that does not include enough information to be reviewed in a timely manner may be closed at the maintainers' discretion. 4 | * All new code requires tests to ensure against regressions 5 | * If you add/remove/modify any event please update the [documentation](docs/events.md) 6 | 7 | ### Description of the Change 8 | 9 | 14 | 15 | ### Alternate Designs 16 | 17 | 18 | 19 | ### Benefits 20 | 21 | 22 | 23 | ### Possible Drawbacks 24 | 25 | 26 | 27 | ### Applicable Issues 28 | 29 | 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ##### Atom and all repositories under Atom will be archived on December 15, 2022. Learn more in our [official announcement](https://github.blog/2022-06-08-sunsetting-atom/) 2 | ## Metrics package 3 | [![OS X Build Status](https://travis-ci.org/atom/metrics.svg?branch=master)](https://travis-ci.org/atom/metrics) [![Windows Build Status](https://ci.appveyor.com/api/projects/status/b5doi205xl3iex04/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/metrics/branch/master) [![Dependency Status](https://david-dm.org/atom/metrics.svg)](https://david-dm.org/atom/metrics) 4 | 5 | Help improve Atom by sending usage statistics, exceptions and deprecations to the team. 6 | 7 | You will be asked at first-run whether you consent to telemetry being sent to the Atom team which includes usage statistics, sanitized exceptions and deprecation warnings. You can change your mind at a later date from the Atom Settings window. 8 | 9 | ### Collected data 10 | 11 | * A unique UUID v4 random identifier is generated according to [RFC4122][RFC4122] 12 | * Screen and window width and height 13 | * Version of Atom being used including which release channel (stable, beta, dev) 14 | * Name of each Atom view class or Atom configuration file opened in a pane, e.g. `EditorView`, `SettingsView`, `MarkdownPreviewView`, and `UserKeymap`. **No other pane item information is collected.** 15 | * Exception messages (without paths) 16 | * Heap memory used as MB and % 17 | * Commands run (except core commands) 18 | * File open events and their language grammar scope names 19 | * Amount of time the current window was open for 20 | * Amount of time the current window took to load 21 | * Amount of time the app took to launch 22 | * Deprecated package names and versions 23 | * Chrome user-agent (version of Chrome, OS, CPU) 24 | * The number of optional (non-bundled) Atom packages activated at startup 25 | * The number of [user-defined key bindings](https://flight-manual.atom.io/using-atom/sections/basic-customization/#customizing-keybindings) loaded at startup 26 | * File save events when editing the [user init script](https://flight-manual.atom.io/hacking-atom/sections/the-init-file/) 27 | * File save events when editing the [user stylesheet](https://flight-manual.atom.io/using-atom/sections/basic-customization/#style-tweaks) 28 | * Repository open events and the hostname from the repository's URL (i.e., `github.com`, `bitbucket.org`, `gitlab.com`, `visualstudio.com`, `amazonaws.com` if the repository is hosted at one of these domains; otherwise, the hostname is anonymized as `other`) 29 | 30 | This information is sent to GitHub's internal analytics pipeline via the [`telemetry`][Telemetry] package which allows the Atom team to analyze usage patterns and errors in order to help improve Atom. 31 | 32 | [Telemetry]: https://github.com/atom/telemetry 33 | [RFC4122]: http://www.ietf.org/rfc/rfc4122.txt 34 | -------------------------------------------------------------------------------- /docs/events.md: -------------------------------------------------------------------------------- 1 | # Events specification 2 | 3 | This document specifies all the data (along with the format) which gets sent from the Atom editor core to the GitHub analytics pipeline. 4 | 5 | This does not include data that's logged by packages on other repositories. Here are links to the events specs of these packages: 6 | 7 | * **Welcome package**: [spec](https://github.com/atom/welcome/blob/master/docs/events.md). 8 | * **Fuzzy finder package**: [spec](https://github.com/atom/fuzzy-finder/blob/master/docs/events.md). 9 | * **GitHub package**: TBD. 10 | 11 | ## Type of events 12 | 13 | There are 3 different types of events: 14 | 15 | * **Counters**: Used to log the number of times a certain event happens. They don't contain any metadata or additional fields. 16 | * **Timing events**: Used to log duration of certain actions. 17 | * **Standard events**: Used to log any other action. 18 | 19 | ## Counters 20 | 21 | These events are used to count how many times a certain action happens. They don't hold any metadata and they only log the name of the counter and the number of times it was incremented. 22 | 23 | Currently Atom core is not logging any counter events, but the [GitHub package](https://github.com/atom/github) is using counters to log things like the number of created PRs. 24 | 25 | ## Timing events 26 | 27 | Timing events log the duration that a specific action took plus some metadata that depends on the event. 28 | 29 | | field | type | description | 30 | |-------|------|-------------| 31 | | `eventType` | `string` | Name of the event/action. 32 | | `date` | `string` | Date when the event happened (In [ISO 8601](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)). 33 | | `durationInMilliseconds` | `number` | The time it took to perform the action. 34 | | `metadata` | `Object` | Any additional metadata. 35 | 36 | 37 | #### Window load time ([more info](https://atom.io/docs/api/v1.35.1/AtomEnvironment#instance-getWindowLoadTime)) 38 | 39 | * **eventType**: `load` 40 | * **metadata** 41 | 42 | | field | value | 43 | |-------|-------| 44 | | `ec` | `core` 45 | 46 | #### Shell load time 47 | 48 | * **eventType**: `load` 49 | * **metadata** 50 | 51 | | field | value | 52 | |-------|-------| 53 | | `ec` | `shell` 54 | 55 | #### Startup markers 56 | 57 | We're logging a bunch of markers to understand where is the startup time spent in Atom. 58 | 59 | A list of all the markers can be found by using [GitHub search](https://github.com/atom/atom/search?q=StartupTime.addMarker%28&unscoped_q=StartupTime.addMarker%28). 60 | 61 | * **eventType**: `startup` 62 | * **metadata** 63 | 64 | | field | value | 65 | |-------|-------| 66 | | `ec` | Label of the marker 67 | 68 | ## Standard events 69 | 70 | Standard events have a free form and can log any data in its `metadata` object. These are the most commonly used types of events. 71 | 72 | | field | type | description | 73 | |-------|------|-------------| 74 | | `eventType` | `string` | Name of the event/action. 75 | | `date` | `string` | Date when the event happened (In [ISO 8601](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)). 76 | | `metadata` | `Object` | Any additional metadata. 77 | 78 | #### Window start/end events 79 | 80 | * **eventType**: `window` 81 | * **metadata** 82 | 83 | | field | value | 84 | |-------|-------| 85 | | `t` | `event` 86 | | `ec` | `window` 87 | | `ea` | `started` 88 | 89 | | field | value | 90 | |-------|-------| 91 | | `t` | `event` 92 | | `ec` | `window` 93 | | `ea` | `ended` 94 | | `ev` | Session duration (in ms). 95 | 96 | #### Open repository 97 | 98 | * **eventType**: `repository` 99 | * **metadata** 100 | 101 | | field | value | 102 | |-------|-------| 103 | | `action` | `open` 104 | | `domain` | `github.com` \| `gitlab.com` \| `bitbucket.org` \| `visualstudio.com` \| `amazonaws.com` \| `other` 105 | 106 | #### Open file 107 | 108 | * **eventType**: `file` 109 | * **metadata** 110 | 111 | | field | value | 112 | |-------|-------| 113 | | `t` | `event` 114 | | `ec` | `file` 115 | | `ea` | `open` 116 | | `el` | `source.${grammarType}` 117 | 118 | #### Execute Atom command 119 | 120 | * **eventType**: `command` 121 | * **metadata** 122 | 123 | | field | value | 124 | |-------|-------| 125 | | `t` | `event` 126 | | `ec` | `command` 127 | | `ea` | First part of the command (until the colon). 128 | | `el` | Executed command 129 | | `ev` | Number of times that command has been executed in this session. 130 | 131 | #### Pane item added 132 | 133 | * **eventType**: `appview` 134 | * **metadata** 135 | 136 | | field | value | 137 | |-------|-------| 138 | | `t` | `appview` 139 | | `cd` | Pane item name. 140 | | `dt` | Pane item grammar. 141 | 142 | #### Number of packages installed 143 | 144 | * **eventType**: `package` 145 | * **metadata** 146 | 147 | | field | value | 148 | |-------|-------| 149 | | `t` | `event` 150 | | `ec` | `package` 151 | | `ea` | `numberOptionalPackagesActivatedAtStartup` 152 | | `ev` | The number of non-bundled active packages at startup. 153 | 154 | #### Number of custom keybindings 155 | 156 | * **eventType**: `key-binding` 157 | * **metadata** 158 | 159 | | field | value | 160 | |-------|-------| 161 | | `t` | `event` 162 | | `ec` | `key-binding` 163 | | `ea` | `numberUserDefinedKeyBindingsLoadedAtStartup` 164 | | `ev` | The number of custom key bindings. 165 | 166 | #### Modify init script file 167 | 168 | * **eventType**: `customization` 169 | * **metadata** 170 | 171 | | field | value | 172 | |-------|-------| 173 | | `t` | `event` 174 | | `ec` | `customization` 175 | | `ea` | `userInitScriptChanged` 176 | 177 | #### Modify user stylesheet 178 | 179 | * **eventType**: `customization` 180 | * **metadata** 181 | 182 | | field | value | 183 | |-------|-------| 184 | | `t` | `event` 185 | | `ec` | `customization` 186 | | `ea` | `userStylesheetChanged` 187 | 188 | #### Metrics consents change 189 | 190 | * **eventType**: `setting` 191 | * **metadata** 192 | 193 | | field | value | 194 | |-------|-------| 195 | | `t` | `event` 196 | | `ec` | `setting` 197 | | `ea` | `core.telemetryConsent` 198 | | `el` | `limited` \| `no` 199 | 200 | #### Deprecation API usage 201 | 202 | * **eventType**: `deprecation-v3` 203 | * **metadata** 204 | 205 | | field | value | 206 | |-------|-------| 207 | | `t` | `event` 208 | | `ec` | `deprecation-v3` 209 | | `ea` | `${packageName}@${version}` (e.g `settings@1.9.2`). 210 | | `el` | deprecation message. 211 | 212 | #### Non-captured error 213 | 214 | * **eventType**: `exception` 215 | * **metadata** 216 | 217 | | field | value | 218 | |-------|------| 219 | | `metadata.t` | `exception` 220 | | `exd` | Exception stack trace. 221 | | `exf` | `0` \| `1` (whether Atom is in Dev mode). 222 | 223 | ## Common metadata fields 224 | 225 | All the timing and the standard events contain some common metadata fields which get always logged: 226 | 227 | | field name | type | description | 228 | |---|---|---| 229 | | `cd2` | `string` | Processor architecture with correct detection of 64-Windows | 230 | | `cd3` | `string` | Processor architecture ([more info](https://nodejs.org/api/process.html#process_process_arch)) | 231 | | `cm1` | `number` | Size of used memory heap (in MiB). 232 | | `cm2` | `number` | Percentage of used heap (from 0-100). 233 | | `sr` | `string` | Screen size in pixels (e.g `1024x768`). 234 | | `vp` | `string` | Atom window size in pixels (e.g `400x300`). 235 | | `aiid` | `string` | Release channel (`stable` \| `beta` \| `dev` \| `unrecognized`). 236 | -------------------------------------------------------------------------------- /lib/metrics.js: -------------------------------------------------------------------------------- 1 | const {CompositeDisposable} = require('atom') 2 | 3 | const path = require('path') 4 | const Reporter = require('./reporter') 5 | const fs = require('fs-plus') 6 | const grim = require('grim') 7 | const {getDomain} = require('./repository-helpers') 8 | 9 | const IgnoredCommands = { 10 | 'vim-mode:move-up': true, 11 | 'vim-mode:move-down': true, 12 | 'vim-mode:move-left': true, 13 | 'vim-mode:move-right': true 14 | } 15 | 16 | module.exports = { 17 | activate ({sessionLength}) { 18 | this.subscriptions = new CompositeDisposable() 19 | this.shouldIncludePanesAndCommands = Math.random() < 0.05 20 | this.begin(sessionLength) 21 | }, 22 | 23 | deactivate () { 24 | if (this.subscriptions != null) this.subscriptions.dispose() 25 | }, 26 | 27 | serialize () { 28 | return { 29 | sessionLength: Date.now() - this.sessionStart 30 | } 31 | }, 32 | 33 | provideReporter () { 34 | return { 35 | incrementCounter: Reporter.incrementCounter.bind(Reporter), 36 | addCustomEvent: Reporter.addCustomEvent.bind(Reporter), 37 | sendEvent: Reporter.sendEvent.bind(Reporter), 38 | sendTiming: Reporter.sendTiming.bind(Reporter), 39 | addTiming: Reporter.addTiming.bind(Reporter), 40 | sendException: Reporter.sendException.bind(Reporter) 41 | } 42 | }, 43 | 44 | begin (sessionLength) { 45 | this.sessionStart = Date.now() 46 | 47 | if (sessionLength) { Reporter.sendEvent('window', 'ended', null, sessionLength) } 48 | Reporter.sendEvent('window', 'started') 49 | 50 | this.subscriptions.add(atom.onDidThrowError((event) => { 51 | let errorMessage = event 52 | if (typeof event !== 'string') { errorMessage = event.message } 53 | errorMessage = stripPath(errorMessage) || 'Unknown' 54 | errorMessage = errorMessage.replace('Uncaught ', '').slice(0, 150) 55 | Reporter.sendException(errorMessage) 56 | })) 57 | 58 | this.subscriptions.add(atom.textEditors.observe((editor) => { 59 | const grammar = editor.getGrammar() 60 | if (grammar) { 61 | Reporter.sendEvent('file', 'open', grammar.scopeName) 62 | } 63 | })) 64 | 65 | this.subscriptions.add(atom.config.onDidChange('core.telemetryConsent', ({newValue, oldValue}) => { 66 | if (newValue !== 'undecided') { 67 | Reporter.sendEvent('setting', 'core.telemetryConsent', newValue) 68 | } 69 | 70 | const notOptedIn = newValue !== 'limited' 71 | Reporter.getStore().setOptOut(notOptedIn) 72 | })) 73 | 74 | this.watchActivationOfOptionalPackages() 75 | this.watchLoadingOfUserDefinedKeyBindings() 76 | this.watchUserInitScriptChanges() 77 | this.watchUserStylesheetChanges() 78 | this.watchPaneItems() 79 | this.watchRepositories() 80 | this.watchCommands() 81 | this.watchDeprecations() 82 | 83 | if (atom.getLoadSettings().shellLoadTime != null) { 84 | // Only send shell load time for the first window 85 | Reporter.addTiming('load', atom.getLoadSettings().shellLoadTime, { ec: 'shell' }) 86 | } 87 | 88 | process.nextTick(() => { 89 | // Wait until window is fully bootstrapped before sending the load time 90 | Reporter.addTiming('load', atom.getWindowLoadTime(), { ec: 'core' }) 91 | 92 | // atom.getStartupMarkers() is not available in the current Atom stable version 93 | // so we need to check for it to make CI pass. This can be removed once v1.37 94 | // is released. 95 | const markers = atom.getStartupMarkers ? atom.getStartupMarkers() : [] 96 | for (const marker of markers) { 97 | Reporter.addTiming('startup', marker.time, { ec: marker.label }) 98 | } 99 | }) 100 | }, 101 | 102 | watchActivationOfOptionalPackages () { 103 | this.subscriptions.add(atom.packages.onDidActivateInitialPackages(() => { 104 | const optionalPackages = atom.packages.getActivePackages().filter((pack) => { 105 | return !atom.packages.isBundledPackage(pack.name) 106 | }) 107 | 108 | Reporter.sendEvent( 109 | 'package', 110 | 'numberOptionalPackagesActivatedAtStartup', 111 | null, 112 | optionalPackages.length 113 | ) 114 | })) 115 | }, 116 | 117 | watchLoadingOfUserDefinedKeyBindings () { 118 | this.subscriptions.add(atom.keymaps.onDidLoadUserKeymap(() => { 119 | const userKeymapPath = atom.keymaps.getUserKeymapPath() 120 | const userDefinedKeyBindings = atom.keymaps.getKeyBindings().filter((binding) => { 121 | return binding.source === userKeymapPath 122 | }) 123 | 124 | Reporter.sendEvent( 125 | 'key-binding', 126 | 'numberUserDefinedKeyBindingsLoadedAtStartup', 127 | null, 128 | userDefinedKeyBindings.length 129 | ) 130 | })) 131 | }, 132 | 133 | watchUserInitScriptChanges () { 134 | this.subscriptions.add(atom.workspace.observeTextEditors((editor) => { 135 | if (editor.getPath() === atom.getUserInitScriptPath()) { 136 | const onDidSaveSubscription = editor.onDidSave(() => 137 | Reporter.sendEvent('customization', 'userInitScriptChanged') 138 | ) 139 | 140 | this.subscriptions.add(editor.onDidDestroy(() => onDidSaveSubscription.dispose())) 141 | } 142 | })) 143 | }, 144 | 145 | watchUserStylesheetChanges () { 146 | this.subscriptions.add(atom.workspace.observeTextEditors((editor) => { 147 | if (editor.getPath() === atom.styles.getUserStyleSheetPath()) { 148 | const onDidSaveSubscription = editor.onDidSave(() => 149 | Reporter.sendEvent('customization', 'userStylesheetChanged') 150 | ) 151 | 152 | this.subscriptions.add(editor.onDidDestroy(() => onDidSaveSubscription.dispose())) 153 | } 154 | })) 155 | }, 156 | 157 | watchPaneItems () { 158 | this.subscriptions.add(atom.workspace.onDidAddPaneItem(({item}) => { 159 | if (!this.shouldIncludePanesAndCommands) return 160 | 161 | Reporter.sendPaneItem(item) 162 | })) 163 | }, 164 | 165 | watchRepositories () { 166 | // TODO Once atom.project.observeRepositories ships to Atom's stable 167 | // channel (likely in Atom 1.30), remove this guard, and update the atom 168 | // engine version in package.json to the first Atom version that includes 169 | // atom.project.observeRepositories 170 | if (atom.project.observeRepositories == null) return 171 | 172 | this.subscriptions.add(atom.project.observeRepositories((repository) => { 173 | const domain = getDomain(repository.getOriginURL()) 174 | Reporter.addCustomEvent('repository', { action: 'open', domain }) 175 | })) 176 | }, 177 | 178 | watchCommands () { 179 | this.subscriptions.add(atom.commands.onWillDispatch(commandEvent => { 180 | if (!this.shouldIncludePanesAndCommands) return 181 | 182 | const {type: eventName} = commandEvent 183 | if (commandEvent.detail != null && commandEvent.detail.jQueryTrigger) return 184 | 185 | if (eventName.startsWith('core:') || eventName.startsWith('editor:')) return 186 | 187 | if (!eventName.includes(':')) return 188 | 189 | if (eventName in IgnoredCommands) return 190 | 191 | Reporter.sendCommand(eventName) 192 | })) 193 | }, 194 | 195 | watchDeprecations () { 196 | let packages 197 | this.deprecationCache = {} 198 | this.packageVersionCache = {} 199 | 200 | atom.packages.onDidActivateInitialPackages(() => { 201 | packages = atom.packages.getLoadedPackages() 202 | for (let pack of packages) { 203 | this.packageVersionCache[pack.name] = getPackageVersion(pack) 204 | } 205 | 206 | // Reports initial deprecations as deprecations may have happened before metrics activation. 207 | setImmediate(() => { 208 | for (let deprecation of grim.getDeprecations()) { 209 | this.reportDeprecation(deprecation) 210 | } 211 | }) 212 | }) 213 | 214 | atom.packages.onDidLoadPackage(pack => { 215 | if (!this.packageVersionCache[pack.name]) { 216 | this.packageVersionCache[pack.name] = getPackageVersion(pack) 217 | } 218 | }) 219 | 220 | grim.on('updated', deprecation => { 221 | setImmediate(() => this.reportDeprecation(deprecation)) 222 | }) 223 | }, 224 | 225 | reportDeprecation (deprecation) { 226 | const message = deprecation.getMessage().slice(0, 500) 227 | 228 | for (let __ in deprecation.stacks) { 229 | const stack = deprecation.stacks[__] 230 | const packageName = (stack.metadata && stack.metadata.packageName) 231 | ? stack.metadata.packageName 232 | : (this.getPackageName(stack) || '').toLowerCase() 233 | if (!packageName) continue 234 | 235 | if (!this.packageVersionCache[packageName]) { 236 | const pack = atom.packages.getLoadedPackage(packageName) 237 | this.packageVersionCache[packageName] = getPackageVersion(pack) 238 | } 239 | 240 | const version = this.packageVersionCache[packageName] 241 | const nameAndVersion = `${packageName}@${version}` 242 | 243 | if (this.deprecationCache[nameAndVersion + message] == null) { 244 | this.deprecationCache[nameAndVersion + message] = true 245 | Reporter.sendEvent('deprecation-v3', nameAndVersion, message) 246 | } 247 | } 248 | }, 249 | 250 | getFileNameFromCallSite (callsite) { 251 | return callsite.fileName != null ? callsite.fileName : callsite.getFileName() 252 | }, 253 | 254 | getPackageName (stack) { 255 | const packagePaths = this.getPackagePathsByPackageName() 256 | 257 | for (let i = 0; i < stack.length; i++) { 258 | const fileName = this.getFileNameFromCallSite(stack[i]) 259 | 260 | // Empty when it was run from the dev console 261 | if (!fileName) return 262 | 263 | // Continue to next stack entry if call is in node_modules 264 | if (fileName.includes(path.sep + 'node_modules' + path.sep)) continue 265 | 266 | for (let packageName in packagePaths) { 267 | const packagePath = packagePaths[packageName] 268 | const relativePath = path.relative(packagePath, fileName) 269 | if (!/^\.\./.test(relativePath)) { 270 | return packageName 271 | } 272 | } 273 | 274 | if (atom.getUserInitScriptPath() === fileName) { 275 | return 'init-script' 276 | } 277 | } 278 | }, 279 | 280 | getPackagePathsByPackageName () { 281 | if (this.packagePathsByPackageName != null) return this.packagePathsByPackageName 282 | 283 | this.packagePathsByPackageName = {} 284 | for (let pack of atom.packages.getLoadedPackages()) { 285 | this.packagePathsByPackageName[pack.name] = pack.path 286 | if ((pack.path.indexOf('.atom/dev/packages') > -1) || (pack.path.indexOf('.atom/packages') > -1)) { 287 | this.packagePathsByPackageName[pack.name] = fs.absolute(pack.path) 288 | } 289 | } 290 | return this.packagePathsByPackageName 291 | } 292 | } 293 | 294 | const PathRE = /'?((\/|\\|[a-z]:\\)[^\s']+)+'?/ig 295 | const stripPath = message => message.replace(PathRE, '') 296 | 297 | const getPackageVersion = function (pack) { 298 | return (pack && pack.metadata && pack.metadata.version) || 'unknown' 299 | } 300 | -------------------------------------------------------------------------------- /lib/reporter.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const telemetry = require('telemetry-github') 3 | 4 | const extend = function (target, ...propertyMaps) { 5 | for (let propertyMap of propertyMaps) { 6 | for (let key in propertyMap) { 7 | const value = propertyMap[key] 8 | target[key] = value 9 | } 10 | } 11 | return target 12 | } 13 | 14 | const getReleaseChannel = function () { 15 | const version = atom.getVersion() 16 | const match = version.match(/\d+\.\d+\.\d+(-([a-z]+)(\d+|-\w{4,})?)?$/) 17 | if (!match) { 18 | return 'unrecognized' 19 | } else if (match[2]) { 20 | return match[2] 21 | } 22 | 23 | return 'stable' 24 | } 25 | 26 | const getOsArch = function () { 27 | // 32-bit node.exe's os.arch() returns 'x86' on 64-Windows 28 | if ((process.platform === 'win32') && (process.env.PROCESSOR_ARCHITEW6432 === 'AMD64')) return 'x64' 29 | 30 | return process.arch 31 | } 32 | 33 | let store 34 | 35 | module.exports = 36 | class Reporter { 37 | static incrementCounter (counterName) { 38 | this.getStore().incrementCounter(counterName) 39 | } 40 | 41 | static addCustomEvent (eventType, event) { 42 | extend(event, this.getEventMetadata()) 43 | this.getStore().addCustomEvent(eventType, event) 44 | } 45 | 46 | static addTiming (eventType, durationInMilliseconds, metadata = {}) { 47 | extend(metadata, this.getEventMetadata()) 48 | this.getStore().addTiming(eventType, durationInMilliseconds, metadata) 49 | } 50 | 51 | // Deprecated: use addCustomEvent instead. 52 | static sendEvent (category, action, label, value) { 53 | const params = { 54 | t: 'event', 55 | ec: category, 56 | ea: action 57 | } 58 | if (label != null) { params.el = label } 59 | if (value != null) { params.ev = value } 60 | 61 | this.addCustomEvent(category, params) 62 | } 63 | 64 | // Deprecated: use addTiming instead. 65 | static sendTiming (category, name, value) { 66 | this.addTiming(name, value, {category}) 67 | } 68 | 69 | static sendException (description) { 70 | const eventType = 'exception' 71 | const params = { 72 | t: eventType, 73 | exd: description, 74 | exf: atom.inDevMode() ? '0' : '1' 75 | } 76 | this.addCustomEvent(eventType, params) 77 | } 78 | 79 | static sendPaneItem (item) { 80 | const eventType = 'appview' 81 | const params = { 82 | t: eventType, 83 | cd: this.viewNameForPaneItem(item) 84 | } 85 | 86 | const grammarName = item.getGrammar && item.getGrammar() && item.getGrammar().name 87 | if (grammarName) { 88 | params.dt = grammarName 89 | } 90 | 91 | this.addCustomEvent(eventType, params) 92 | } 93 | 94 | static sendCommand (commandName) { 95 | const eventType = 'command' 96 | if (this.commandCount == null) { this.commandCount = {} } 97 | if (this.commandCount[commandName] == null) { this.commandCount[commandName] = 0 } 98 | this.commandCount[commandName]++ 99 | 100 | const params = { 101 | t: 'event', 102 | ec: eventType, 103 | ea: commandName.split(':')[0], 104 | el: commandName, 105 | ev: this.commandCount[commandName] 106 | } 107 | 108 | this.addCustomEvent(eventType, params) 109 | } 110 | 111 | // Private 112 | static getStore () { 113 | if (!store) { 114 | store = new telemetry.StatsStore( 115 | 'atom', 116 | atom.getVersion(), 117 | atom.inDevMode(), 118 | () => {}, 119 | { 120 | logInDevMode: atom.config.get('metrics.dev.sendMetricsInDevMode'), 121 | reportingFrequency: atom.config.get('metrics.dev.reportingFrequency') * 60 * 1000, 122 | verboseMode: atom.config.get('metrics.dev.verboseMode') 123 | } 124 | ) 125 | const notOptedIn = atom.config.get('core.telemetryConsent') !== 'limited' 126 | store.setOptOut(notOptedIn) 127 | } 128 | return store 129 | } 130 | 131 | // Private 132 | static consented () { 133 | return atom.config.get('core.telemetryConsent') === 'limited' 134 | } 135 | 136 | // Private 137 | static isTelemetryConsentChoice (params) { 138 | return (params.t === 'event') && (params.ec === 'setting') && (params.ea === 'core.telemetryConsent') 139 | } 140 | 141 | // Private 142 | static getEventMetadata () { 143 | const memUse = process.memoryUsage() 144 | return { 145 | // cd1: was start date, removed 146 | cd2: getOsArch(), 147 | cd3: process.arch, 148 | cm1: memUse.heapUsed >> 20, // Convert bytes to megabytes 149 | cm2: Math.round((memUse.heapUsed / memUse.heapTotal) * 100), 150 | sr: `${window.screen.width}x${window.screen.height}`, 151 | vp: `${window.innerWidth}x${window.innerHeight}`, 152 | aiid: getReleaseChannel() 153 | } 154 | } 155 | 156 | // Private 157 | static viewNameForPaneItem (item) { 158 | let name = (item.getViewClass && item.getViewClass().name) || item.constructor.name 159 | const itemPath = item.getPath && item.getPath() 160 | 161 | if ((itemPath == null) || (path.dirname(itemPath) !== atom.getConfigDirPath())) { return name } 162 | 163 | const extension = path.extname(itemPath) 164 | switch (path.basename(itemPath, extension)) { 165 | case 'config': 166 | if (['.json', '.cson'].includes(extension)) { name = 'UserConfig' } 167 | break 168 | case 'init': 169 | if (['.js', '.coffee'].includes(extension)) { name = 'UserInitScript' } 170 | break 171 | case 'keymap': 172 | if (['.json', '.cson'].includes(extension)) { name = 'UserKeymap' } 173 | break 174 | case 'snippets': 175 | if (['.json', '.cson'].includes(extension)) { name = 'UserSnippets' } 176 | break 177 | case 'styles': 178 | if (['.css', '.less'].includes(extension)) { name = 'UserStylesheet' } 179 | break 180 | } 181 | return name 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /lib/repository-helpers.js: -------------------------------------------------------------------------------- 1 | const getDomain = function (gitURL) { 2 | const patternsToDomains = [ 3 | [/(https:\/\/|@)github\.com/, 'github.com'], 4 | [/(https:\/\/|@)gitlab\.com/, 'gitlab.com'], 5 | [/(https:\/\/|@)bitbucket\.org/, 'bitbucket.org'], 6 | [/(https:\/\/|@).*\.visualstudio\.com/, 'visualstudio.com'], 7 | [/(https:\/\/|@)git-codecommit\..*\.amazonaws\.com/, 'amazonaws.com'] 8 | ] 9 | 10 | for (let [pattern, domain] of patternsToDomains) { 11 | if (pattern.test(gitURL)) return domain 12 | } 13 | 14 | return 'other' 15 | } 16 | 17 | module.exports = {getDomain} 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "metrics", 3 | "main": "./lib/metrics", 4 | "version": "1.8.1", 5 | "description": "Help improve Atom by sending usage statistics, exceptions and deprecations to the team", 6 | "repository": "https://github.com/atom/metrics", 7 | "license": "MIT", 8 | "engines": { 9 | "atom": ">0.50.0" 10 | }, 11 | "providedServices": { 12 | "metrics-reporter": { 13 | "versions": { 14 | "1.1.0": "provideReporter" 15 | } 16 | } 17 | }, 18 | "dependencies": { 19 | "fs-plus": "^3.0.0", 20 | "grim": "^2.0.1", 21 | "telemetry-github": "0.1.1" 22 | }, 23 | "devDependencies": { 24 | "standard": "^11.0.1", 25 | "temp": "^0.8.3" 26 | }, 27 | "standard": { 28 | "env": [ 29 | "browser", 30 | "node", 31 | "atomtest", 32 | "jasmine" 33 | ], 34 | "globals": [ 35 | "atom" 36 | ] 37 | }, 38 | "configSchema": { 39 | "dev": { 40 | "title": "Development Settings", 41 | "description": "Please refresh the Atom window after modifying any setting in this section.", 42 | "type": "object", 43 | "properties": { 44 | "verboseMode": { 45 | "title": "Verbose mode", 46 | "type": "boolean", 47 | "default": false, 48 | "description": "When set to `true`, any performed request containing metrics will be logged in console.", 49 | "order": 1 50 | }, 51 | "sendMetricsInDevMode": { 52 | "title": "Send metrics in Dev mode", 53 | "type": "boolean", 54 | "default": false, 55 | "description": "By default, Atom does not log anything in Dev mode. Set this param to `true` to log events in dev mode.", 56 | "order": 2 57 | }, 58 | "reportingFrequency": { 59 | "title": "Reporting frequency", 60 | "type": "number", 61 | "default": 1440, 62 | "description": "How often does Atom send metrics to GitHub (in minutes). By default it's once a day.", 63 | "order": 3 64 | } 65 | } 66 | } 67 | }, 68 | "scripts": { 69 | "lint": "standard --verbose", 70 | "test": "npm run lint && apm test" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /spec/fixtures/keymaps/custom-keymap.cson: -------------------------------------------------------------------------------- 1 | # Your keymap 2 | # 3 | # Atom keymaps work similarly to style sheets. Just as style sheets use 4 | # selectors to apply styles to elements, Atom keymaps use selectors to associate 5 | # keystrokes with events in specific contexts. Unlike style sheets however, 6 | # each selector can only be declared once. 7 | # 8 | # You can create a new keybinding in this file by typing "key" and then hitting 9 | # tab. 10 | # 11 | # Here's an example taken from Atom's built-in keymap: 12 | # 13 | # 'atom-text-editor': 14 | # 'enter': 'editor:newline' 15 | # 16 | # 'atom-workspace': 17 | # 'ctrl-shift-p': 'core:move-up' 18 | # 'ctrl-p': 'core:move-down' 19 | # 20 | # You can find more information about keymaps in these guides: 21 | # * http://flight-manual.atom.io/using-atom/sections/basic-customization/#customizing-keybindings 22 | # * http://flight-manual.atom.io/behind-atom/sections/keymaps-in-depth/ 23 | # 24 | # If you're having trouble with your keybindings not working, try the 25 | # Keybinding Resolver: `Cmd+.` on macOS and `Ctrl+.` on other platforms. See the 26 | # Debugging Guide for more information: 27 | # * http://flight-manual.atom.io/hacking-atom/sections/debugging/#check-the-keybindings 28 | # 29 | # This file uses CoffeeScript Object Notation (CSON). 30 | # If you are unfamiliar with CSON, you can read more about it in the 31 | # Atom Flight Manual: 32 | # http://flight-manual.atom.io/using-atom/sections/basic-customization/#configuring-with-cson 33 | 34 | 'atom-workspace atom-pane, atom-workspace atom-text-editor:not(.mini)': 35 | 'ctrl-|': 'pane:split-right-and-copy-active-item' 36 | 'ctrl--': 'pane:split-down-and-copy-active-item' 37 | 38 | 'atom-workspace, .command-palette atom-text-editor': 39 | 'cmd-p': 'command-palette:toggle' 40 | -------------------------------------------------------------------------------- /spec/fixtures/keymaps/default-keymap.cson: -------------------------------------------------------------------------------- 1 | # Your keymap 2 | # 3 | # Atom keymaps work similarly to style sheets. Just as style sheets use 4 | # selectors to apply styles to elements, Atom keymaps use selectors to associate 5 | # keystrokes with events in specific contexts. Unlike style sheets however, 6 | # each selector can only be declared once. 7 | # 8 | # You can create a new keybinding in this file by typing "key" and then hitting 9 | # tab. 10 | # 11 | # Here's an example taken from Atom's built-in keymap: 12 | # 13 | # 'atom-text-editor': 14 | # 'enter': 'editor:newline' 15 | # 16 | # 'atom-workspace': 17 | # 'ctrl-shift-p': 'core:move-up' 18 | # 'ctrl-p': 'core:move-down' 19 | # 20 | # You can find more information about keymaps in these guides: 21 | # * http://flight-manual.atom.io/using-atom/sections/basic-customization/#customizing-keybindings 22 | # * http://flight-manual.atom.io/behind-atom/sections/keymaps-in-depth/ 23 | # 24 | # If you're having trouble with your keybindings not working, try the 25 | # Keybinding Resolver: `Cmd+.` on macOS and `Ctrl+.` on other platforms. See the 26 | # Debugging Guide for more information: 27 | # * http://flight-manual.atom.io/hacking-atom/sections/debugging/#check-the-keybindings 28 | # 29 | # This file uses CoffeeScript Object Notation (CSON). 30 | # If you are unfamiliar with CSON, you can read more about it in the 31 | # Atom Flight Manual: 32 | # http://flight-manual.atom.io/using-atom/sections/basic-customization/#configuring-with-cson 33 | -------------------------------------------------------------------------------- /spec/fixtures/packages/example/lib/example.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | activate () {} 3 | } 4 | -------------------------------------------------------------------------------- /spec/helpers/async-spec-helpers.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | const {now} = Date 4 | const {setTimeout} = global 5 | 6 | export function beforeEach (fn) { 7 | global.beforeEach(function () { 8 | const result = fn() 9 | if (result instanceof Promise) { 10 | waitsForPromise(() => result) 11 | } 12 | }) 13 | } 14 | 15 | export function afterEach (fn) { 16 | global.afterEach(function () { 17 | const result = fn() 18 | if (result instanceof Promise) { 19 | waitsForPromise(() => result) 20 | } 21 | }) 22 | } 23 | 24 | ['it', 'fit', 'ffit', 'fffit'].forEach(function (name) { 25 | module.exports[name] = function (description, fn) { 26 | global[name](description, function () { 27 | const result = fn() 28 | if (result instanceof Promise) { 29 | waitsForPromise(() => result) 30 | } 31 | }) 32 | } 33 | }) 34 | 35 | export async function conditionPromise (condition) { 36 | const startTime = now() 37 | 38 | while (true) { 39 | await timeoutPromise(100) 40 | 41 | if (await condition()) { 42 | return 43 | } 44 | 45 | if (now() - startTime > 5000) { 46 | throw new Error('Timed out waiting on condition') 47 | } 48 | } 49 | } 50 | 51 | export function timeoutPromise (timeout) { 52 | return new Promise(function (resolve) { 53 | setTimeout(resolve, timeout) 54 | }) 55 | } 56 | 57 | function waitsForPromise (fn) { 58 | const promise = fn() 59 | global.waitsFor('spec promise to resolve', function (done) { 60 | promise.then(done, function (error) { 61 | jasmine.getEnv().currentSpec.fail(error) 62 | done() 63 | }) 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /spec/metrics-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import {it, fit, ffit, fffit, beforeEach, afterEach, conditionPromise} from './helpers/async-spec-helpers' // eslint-disable-line no-unused-vars 4 | import Reporter from '../lib/reporter' 5 | import fs from 'fs-plus' 6 | import grim from 'grim' 7 | import path from 'path' 8 | import temp from 'temp' 9 | 10 | temp.track() 11 | 12 | const telemetry = require('telemetry-github') 13 | 14 | const store = new telemetry.StatsStore('atom', '1.2.3', true) 15 | 16 | describe('Metrics', () => { 17 | let workspaceElement = [] 18 | 19 | const assertCommandNotReported = (commandName, additionalArgs) => { 20 | atom.commands.dispatch(workspaceElement, commandName, additionalArgs) 21 | expect(Reporter.addCustomEvent).not.toHaveBeenCalled() 22 | } 23 | 24 | const eventReportedPromise = ({category, action, value}) => { 25 | return conditionPromise(() => { 26 | return Reporter.addCustomEvent.calls.find((call) => { 27 | const eventType = call.args[0] 28 | const eventObject = call.args[1] 29 | return eventType === category && 30 | eventObject.t === 'event' && 31 | eventObject.ea === action && 32 | eventObject.ev === value 33 | }) 34 | }) 35 | } 36 | 37 | beforeEach(() => { 38 | workspaceElement = atom.views.getView(atom.workspace) 39 | 40 | spyOn(Reporter, 'addCustomEvent').andCallThrough() 41 | spyOn(Reporter, 'getStore').andCallFake(() => store) 42 | 43 | let storage = {} 44 | spyOn(global.localStorage, 'setItem').andCallFake((key, value) => { storage[key] = value }) 45 | spyOn(global.localStorage, 'getItem').andCallFake(key => storage[key]) 46 | 47 | Reporter.commandCount = undefined 48 | spyOn(Reporter, 'consented').andReturn(true) 49 | }) 50 | 51 | afterEach(async () => { 52 | atom.packages.deactivatePackage('metrics') 53 | }) 54 | 55 | it('reports consent opt-out changes', async () => { 56 | await atom.packages.activatePackage('metrics') 57 | spyOn(store, 'setOptOut') 58 | spyOn(Reporter, 'sendEvent') 59 | await atom.config.set('core.telemetryConsent', 'no') 60 | 61 | expect(Reporter.sendEvent.mostRecentCall.args).toEqual(['setting', 'core.telemetryConsent', 'no']) 62 | expect(store.setOptOut.mostRecentCall.args[0]).toEqual(true) 63 | }) 64 | 65 | it('reports consent opt-in changes', async () => { 66 | await atom.packages.activatePackage('metrics') 67 | spyOn(store, 'setOptOut') 68 | spyOn(Reporter, 'sendEvent') 69 | await atom.config.set('core.telemetryConsent', 'limited') 70 | 71 | expect(Reporter.sendEvent.mostRecentCall.args).toEqual(['setting', 'core.telemetryConsent', 'limited']) 72 | expect(store.setOptOut.mostRecentCall.args[0]).toEqual(false) 73 | }) 74 | 75 | describe('event metadata', async () => { 76 | beforeEach(() => { 77 | spyOn(store, 'addTiming') 78 | }) 79 | const assertMetadataSent = async (expectedName, expectedValue) => { 80 | await atom.packages.activatePackage('metrics') 81 | 82 | await conditionPromise(() => Reporter.addCustomEvent.callCount > 0) 83 | let metadata = Reporter.addCustomEvent.mostRecentCall.args[1] 84 | expect(metadata[expectedName]).toEqual(expectedValue) 85 | 86 | await conditionPromise(() => store.addTiming.callCount > 0) 87 | metadata = store.addTiming.mostRecentCall.args[2] 88 | expect(metadata[expectedName]).toEqual(expectedValue) 89 | } 90 | it('reports actual processor architecture', async () => { 91 | const expectedArch = process.env.PROCESSOR_ARCHITEW6432 === 'AMD64' ? 'x64' : process.arch 92 | await assertMetadataSent('cd2', expectedArch) 93 | }) 94 | 95 | it('specifies screen resolution', async () => { 96 | const expectedScreenResolution = `${window.screen.width}x${window.screen.height}` 97 | await assertMetadataSent('sr', expectedScreenResolution) 98 | }) 99 | 100 | it('specifies window resolution', async () => { 101 | const expectedWindowResolution = `${window.innerWidth}x${window.innerHeight}` 102 | await assertMetadataSent('vp', expectedWindowResolution) 103 | }) 104 | 105 | it('specifies heap usage in MB and %', async () => { 106 | spyOn(process, 'memoryUsage').andReturn({heapTotal: 234567890, heapUsed: 123456789}) 107 | 108 | const heapUsedInMb = 117 109 | const heapUsedPercentage = 53 110 | await assertMetadataSent('cm1', heapUsedInMb) 111 | await assertMetadataSent('cm2', heapUsedPercentage) 112 | }) 113 | }) 114 | 115 | describe('reporting release channel', async () => { 116 | beforeEach(() => global.localStorage.setItem('metrics.userId', 'a')) 117 | 118 | it('reports the dev release channel', async () => { 119 | spyOn(atom, 'getVersion').andReturn('1.0.2-dev-dedbeef') 120 | 121 | await atom.packages.activatePackage('metrics') 122 | let event = Reporter.addCustomEvent.mostRecentCall.args[1] 123 | expect(event.aiid).toEqual('dev') 124 | }) 125 | 126 | it('reports the beta release channel', async () => { 127 | spyOn(atom, 'getVersion').andReturn('1.0.2-beta1') 128 | 129 | await atom.packages.activatePackage('metrics') 130 | let event = Reporter.addCustomEvent.mostRecentCall.args[1] 131 | expect(event.aiid).toEqual('beta') 132 | }) 133 | 134 | it('reports the stable release channel', async () => { 135 | spyOn(atom, 'getVersion').andReturn('1.0.2') 136 | 137 | await atom.packages.activatePackage('metrics') 138 | 139 | let event = Reporter.addCustomEvent.mostRecentCall.args[1] 140 | expect(event.aiid).toEqual('stable') 141 | }) 142 | 143 | it('reports the nightly release channel', async () => { 144 | spyOn(atom, 'getVersion').andReturn('1.0.2-nightly55') 145 | 146 | await atom.packages.activatePackage('metrics') 147 | 148 | let event = Reporter.addCustomEvent.mostRecentCall.args[1] 149 | expect(event.aiid).toEqual('nightly') 150 | }) 151 | 152 | it('reports an arbitrary release channel', async () => { 153 | spyOn(atom, 'getVersion').andReturn('1.0.2-sushi1') 154 | 155 | await atom.packages.activatePackage('metrics') 156 | 157 | let event = Reporter.addCustomEvent.mostRecentCall.args[1] 158 | expect(event.aiid).toEqual('sushi') 159 | }) 160 | 161 | it('reports an unrecognized release channel', async () => { 162 | spyOn(atom, 'getVersion').andReturn('wat.0.2') 163 | 164 | await atom.packages.activatePackage('metrics') 165 | 166 | let event = Reporter.addCustomEvent.mostRecentCall.args[1] 167 | expect(event.aiid).toEqual('unrecognized') 168 | }) 169 | }) 170 | 171 | describe('reporting commands', async () => { 172 | describe('when shouldIncludePanesAndCommands is false', async () => { 173 | beforeEach(async () => { 174 | global.localStorage.setItem('metrics.userId', 'a') 175 | await atom.packages.activatePackage('metrics') 176 | 177 | await conditionPromise(() => Reporter.addCustomEvent.callCount > 0) 178 | 179 | const {mainModule} = atom.packages.getLoadedPackage('metrics') 180 | mainModule.shouldIncludePanesAndCommands = false 181 | }) 182 | 183 | it('does not watch for commands', async () => { 184 | let command = 'some-package:a-command' 185 | 186 | atom.commands.dispatch(workspaceElement, command, null) 187 | expect(Reporter.commandCount).toBeUndefined() 188 | }) 189 | }) 190 | 191 | describe('when shouldIncludePanesAndCommands is true', async () => { 192 | beforeEach(async () => { 193 | global.localStorage.setItem('metrics.userId', 'd') 194 | await atom.packages.activatePackage('metrics') 195 | 196 | const {mainModule} = atom.packages.getLoadedPackage('metrics') 197 | mainModule.shouldIncludePanesAndCommands = true 198 | Reporter.addCustomEvent.reset() 199 | }) 200 | 201 | it('reports commands dispatched via atom.commands', () => { 202 | let command = 'some-package:a-command' 203 | 204 | atom.commands.dispatch(workspaceElement, command, null) 205 | expect(Reporter.commandCount[command]).toBe(1) 206 | 207 | const args = Reporter.addCustomEvent.mostRecentCall.args 208 | expect(args[0]).toEqual('command') 209 | let event = args[1] 210 | expect(event.t).toEqual('event') 211 | expect(event.ec).toEqual('command') 212 | expect(event.ea).toEqual('some-package') 213 | expect(event.el).toEqual('some-package:a-command') 214 | 215 | atom.commands.dispatch(workspaceElement, command, null) 216 | expect(Reporter.commandCount[command]).toBe(2) 217 | 218 | expect(Reporter.addCustomEvent.mostRecentCall.args[1].ev).toEqual(2) 219 | }) 220 | 221 | it('does not report editor: and core: commands', () => { 222 | assertCommandNotReported('core:move-up') 223 | assertCommandNotReported('editor:move-to-end-of-line') 224 | }) 225 | 226 | it('does not report non-namespaced commands', () => { 227 | assertCommandNotReported('dragover') 228 | }) 229 | 230 | it('does not report vim-mode:* movement commands', () => { 231 | assertCommandNotReported('vim-mode:move-up') 232 | assertCommandNotReported('vim-mode:move-down') 233 | assertCommandNotReported('vim-mode:move-left') 234 | assertCommandNotReported('vim-mode:move-right') 235 | }) 236 | 237 | it('does not report commands triggered via jquery', () => { 238 | assertCommandNotReported('some-package:a-command', {jQueryTrigger: 'trigger'}) 239 | }) 240 | }) 241 | }) 242 | 243 | describe('reporting timings', async () => { 244 | it('reports timing metrics', async () => { 245 | spyOn(Reporter, 'addTiming') 246 | 247 | await atom.packages.activatePackage('metrics') 248 | const expectedLoadTime = atom.getWindowLoadTime() 249 | 250 | const addTimingArgs = Reporter.addTiming.calls[0].args 251 | expect(addTimingArgs[0]).toEqual('load') 252 | expect(addTimingArgs[1]).toEqual(expectedLoadTime) 253 | expect(addTimingArgs[2]).toEqual({ec: 'core'}) 254 | }) 255 | }) 256 | 257 | describe('reporting exceptions', async () => { 258 | const assertException = function (args, expectedMessage) { 259 | expect(args[0]).toEqual('exception') 260 | const event = args[1] 261 | expect(event.exd).toContain(expectedMessage) 262 | } 263 | beforeEach(async () => { 264 | spyOn(atom, 'openDevTools').andReturn(Promise.resolve()) 265 | spyOn(atom, 'executeJavaScriptInDevTools') 266 | await atom.packages.activatePackage('metrics') 267 | await conditionPromise(() => Reporter.addCustomEvent.callCount > 0) 268 | }) 269 | 270 | it('reports an exception with the correct type', () => { 271 | let message = "Uncaught TypeError: Cannot call method 'getScreenRow' of undefined" 272 | window.onerror(message, 'abc', 2, 3, {ok: true}) 273 | 274 | assertException(Reporter.addCustomEvent.mostRecentCall.args, 'TypeError') 275 | }) 276 | 277 | describe('when the message has no clear type', () => 278 | it('reports an exception with the correct type', () => { 279 | let message = '' 280 | window.onerror(message, 2, 3, {ok: true}) 281 | 282 | assertException(Reporter.addCustomEvent.mostRecentCall.args, 'Unknown') 283 | }) 284 | ) 285 | 286 | describe('when there are paths in the exception', () => { 287 | it('strips unix paths surrounded in quotes', () => { 288 | let message = "Error: ENOENT, unlink '/Users/someuser/path/file.js'" 289 | window.onerror(message, 2, 3, {ok: true}) 290 | assertException(Reporter.addCustomEvent.mostRecentCall.args, 'Error: ENOENT, unlink ') 291 | }) 292 | 293 | it('strips unix paths without quotes', () => { 294 | let message = 'Uncaught Error: spawn /Users/someuser.omg/path/file-09238_ABC-Final-Final.js ENOENT' 295 | window.onerror(message, 2, 3, {ok: true}) 296 | assertException(Reporter.addCustomEvent.mostRecentCall.args, 'Error: spawn ENOENT') 297 | }) 298 | 299 | it('strips windows paths without quotes', () => { 300 | let message = 'Uncaught Error: spawn c:\\someuser.omg\\path\\file-09238_ABC-Fin%%$#()al-Final.js ENOENT' 301 | window.onerror(message, 2, 3, {ok: true}) 302 | assertException(Reporter.addCustomEvent.mostRecentCall.args, 'Error: spawn ENOENT') 303 | }) 304 | 305 | it('strips windows paths surrounded in quotes', () => { 306 | let message = "Uncaught Error: EACCES 'c:\\someuser.omg\\path\\file-09238_ABC-Fin%%$#()al-Final.js'" 307 | window.onerror(message, 2, 3, {ok: true}) 308 | assertException(Reporter.addCustomEvent.mostRecentCall.args, 'Error: EACCES ') 309 | }) 310 | }) 311 | }) 312 | 313 | describe('reporting deprecations', async () => { 314 | beforeEach(async () => { 315 | await atom.packages.activatePackage('metrics') 316 | await conditionPromise(() => Reporter.addCustomEvent.callCount > 0) 317 | }) 318 | 319 | it('reports a deprecation with metadata specified', async () => { 320 | jasmine.snapshotDeprecations() 321 | const deprecationMessage = 'bad things are bad' 322 | grim.deprecate(deprecationMessage, {packageName: 'somepackage'}) 323 | jasmine.restoreDeprecationsSnapshot() 324 | 325 | await conditionPromise(() => Reporter.addCustomEvent.callCount > 0) 326 | const args = Reporter.addCustomEvent.mostRecentCall.args 327 | expect(args[0]).toEqual('deprecation-v3') 328 | 329 | const eventObject = args[1] 330 | expect(eventObject.t).toEqual('event') 331 | expect(eventObject.ec).toEqual('deprecation-v3') 332 | expect(eventObject.ea).toEqual('somepackage@unknown') 333 | expect(eventObject.el).toEqual(deprecationMessage) 334 | }) 335 | 336 | it('reports a deprecation without metadata specified', async () => { 337 | jasmine.snapshotDeprecations() 338 | 339 | let stack = [ 340 | { 341 | fileName: '/Applications/Atom.app/pathwatcher.js', 342 | functionName: 'foo', 343 | location: '/Applications/Atom.app/pathwatcher.js:10:5' 344 | }, 345 | { 346 | fileName: '/Users/me/.atom/packages/metrics/lib/metrics.js', 347 | functionName: 'bar', 348 | location: '/Users/me/.atom/packages/metrics/lib/metrics.js:161:5' 349 | } 350 | ] 351 | let deprecation = { 352 | message: 'bad things are bad', 353 | stacks: [stack] 354 | } 355 | grim.addSerializedDeprecation(deprecation) 356 | 357 | spyOn(atom.packages.getLoadedPackage('metrics').mainModule, 'getPackagePathsByPackageName').andReturn({ 358 | metrics: '/Users/me/.atom/packages/metrics'}) 359 | 360 | jasmine.restoreDeprecationsSnapshot() 361 | 362 | await conditionPromise(() => Reporter.addCustomEvent.callCount > 0) 363 | const args = Reporter.addCustomEvent.mostRecentCall.args 364 | expect(args[0]).toEqual('deprecation-v3') 365 | const event = args[1] 366 | expect(event.ec).toEqual('deprecation-v3') 367 | expect(event.el).toEqual('bad things are bad') 368 | }) 369 | }) 370 | 371 | describe('reporting pane items', async () => { 372 | describe('when shouldIncludePanesAndCommands is false', async () => { 373 | beforeEach(async () => { 374 | spyOn(Reporter, 'sendPaneItem') 375 | spyOn(Reporter, 'sendEvent') 376 | 377 | const {mainModule} = await atom.packages.activatePackage('metrics') 378 | 379 | mainModule.shouldIncludePanesAndCommands = false 380 | }) 381 | 382 | it('will not report pane items', async () => { 383 | Reporter.addCustomEvent.reset() 384 | Reporter.sendEvent.reset() 385 | Reporter.sendPaneItem.reset() 386 | await atom.packages.emitter.emit('did-add-pane') 387 | 388 | expect(Reporter.sendPaneItem.callCount).toBe(0) 389 | expect(Reporter.sendEvent.callCount).toBe(0) 390 | expect(Reporter.addCustomEvent.callCount).toBe(0) 391 | }) 392 | }) 393 | 394 | describe('when shouldIncludePanesAndCommands is true', async () => { 395 | beforeEach(async () => { 396 | const {mainModule} = await atom.packages.activatePackage('metrics') 397 | mainModule.shouldIncludePanesAndCommands = true 398 | 399 | await conditionPromise(() => Reporter.addCustomEvent.callCount > 0) 400 | }) 401 | 402 | it('will report pane items', async () => { 403 | await atom.workspace.open('file1.txt') 404 | const paneItemCalls = Reporter.addCustomEvent.calls.filter((call) => { 405 | const eventType = call.args[0] 406 | const event = call.args[1] 407 | return eventType === 'appview' && event.cd === 'TextEditor' 408 | }) 409 | expect(paneItemCalls.length).toBe(1) 410 | }) 411 | }) 412 | }) 413 | 414 | describe('reporting repositories', () => { 415 | it('reports when a repository gets opened', async () => { 416 | // TODO Once atom.project.observeRepositories ships to Atom's stable 417 | // channel (likely in Atom 1.30), remove this guard, and update the atom 418 | // engine version in package.json to the first Atom version that includes 419 | // atom.project.observeRepositories 420 | if (atom.project.observeRepositories == null) return 421 | 422 | await atom.packages.activatePackage('metrics') 423 | Reporter.addCustomEvent.reset() 424 | 425 | const repositoryPath = path.join(__dirname, '..') 426 | atom.project.addPath(repositoryPath) 427 | 428 | await conditionPromise(() => { 429 | return Reporter.addCustomEvent.calls.find((call) => { 430 | const eventType = call.args[0] 431 | const eventObject = call.args[1] 432 | return eventType === 'repository' && 433 | eventObject.action === 'open' && 434 | eventObject.domain === 'github.com' 435 | }) 436 | }) 437 | }) 438 | }) 439 | 440 | describe('reporting activation of optional packages', () => { 441 | describe('when optional packages are present', () => { 442 | let originalPackageDirPaths = atom.packages.packageDirPaths 443 | 444 | beforeEach(() => { 445 | const packageFixturePath = path.join(__dirname, 'fixtures', 'packages') 446 | atom.packages.packageDirPaths.push(packageFixturePath) 447 | }) 448 | 449 | it('reports the number of optional packages activated at startup', async () => { 450 | await atom.packages.activatePackage('metrics') 451 | expect(atom.packages.isBundledPackage('metrics')).toBe(true) 452 | 453 | await atom.packages.activatePackage('example') 454 | expect(atom.packages.isBundledPackage('example')).toBe(false) 455 | 456 | // Mimic the event that is emitted when Atom finishes loading all 457 | // packages at startup. (We don't want to weigh down this test with the 458 | // overhead of actually load _all_ packages.) 459 | atom.packages.emitter.emit('did-activate-initial-packages') 460 | 461 | await eventReportedPromise({ 462 | 'category': 'package', 463 | 'action': 'numberOptionalPackagesActivatedAtStartup', 464 | 'value': 1 465 | }) 466 | }) 467 | 468 | afterEach(() => { 469 | atom.packages.packageDirPaths = originalPackageDirPaths 470 | }) 471 | }) 472 | 473 | describe('when only bundled packages are present', () => { 474 | it('reports a quantity of zero when the user has no optional packages enabled', async () => { 475 | await atom.packages.activatePackage('metrics') 476 | expect(atom.packages.isBundledPackage('metrics')).toBe(true) 477 | 478 | // Mimic the event that is emitted when Atom finishes loading all 479 | // packages at startup. (We don't want to weigh down this test with the 480 | // overhead of actually load _all_ packages.) 481 | atom.packages.emitter.emit('did-activate-initial-packages') 482 | 483 | await eventReportedPromise({ 484 | 'category': 'package', 485 | 'action': 'numberOptionalPackagesActivatedAtStartup', 486 | 'value': 0 487 | }) 488 | }) 489 | }) 490 | }) 491 | 492 | describe('reporting presence of user-defined key bindings', () => { 493 | describe('when user-defined key bindings are present', () => { 494 | it('reports the number of user-defined key bindings loaded at startup', async () => { 495 | await atom.packages.activatePackage('metrics') 496 | 497 | // Manually trigger the keymap loading that Atom performs at startup. 498 | // (We don't want to weigh down this test with running through the 499 | // entire Atom startup process.) 500 | const keymapFixturePath = path.join(__dirname, 'fixtures', 'keymaps', 'custom-keymap.cson') 501 | spyOn(atom.keymaps, 'getUserKeymapPath').andReturn(keymapFixturePath) 502 | atom.keymaps.loadUserKeymap() 503 | 504 | await eventReportedPromise({ 505 | 'category': 'key-binding', 506 | 'action': 'numberUserDefinedKeyBindingsLoadedAtStartup', 507 | 'value': 3 508 | }) 509 | }) 510 | 511 | afterEach(() => { 512 | atom.keymaps.destroy() 513 | }) 514 | }) 515 | 516 | describe('when no user-defined key bindings are present', () => { 517 | it('reports that zero user-defined key bindings were loaded', async () => { 518 | await atom.packages.activatePackage('metrics') 519 | 520 | // Manually trigger the keymap loading that Atom performs at startup. 521 | // (We don't want to weigh down this test with running through the 522 | // entire Atom startup process.) 523 | const keymapFixturePath = path.join(__dirname, 'fixtures', 'keymaps', 'default-keymap.cson') 524 | spyOn(atom.keymaps, 'getUserKeymapPath').andReturn(keymapFixturePath) 525 | atom.keymaps.loadUserKeymap() 526 | 527 | await eventReportedPromise({ 528 | 'category': 'key-binding', 529 | 'action': 'numberUserDefinedKeyBindingsLoadedAtStartup', 530 | 'value': 0 531 | }) 532 | }) 533 | 534 | afterEach(() => { 535 | atom.keymaps.destroy() 536 | }) 537 | }) 538 | }) 539 | 540 | describe('reporting customization of user init script', () => { 541 | it('reports event when init script changes', async () => { 542 | const tempDir = fs.realpathSync(temp.mkdirSync()) 543 | const userInitScriptPath = path.join(tempDir, 'init.js') 544 | fs.writeFileSync(userInitScriptPath, '') 545 | 546 | spyOn(atom, 'getUserInitScriptPath').andReturn(userInitScriptPath) 547 | 548 | await atom.packages.activatePackage('metrics') 549 | 550 | const editor = await atom.workspace.open(userInitScriptPath) 551 | editor.setText("console.log('hello world')") 552 | editor.save() 553 | 554 | await eventReportedPromise({ 555 | 'category': 'customization', 556 | 'action': 'userInitScriptChanged' 557 | }) 558 | }) 559 | }) 560 | 561 | describe('reporting customization of user stylesheet', () => { 562 | it('reports event when stylesheet changes', async () => { 563 | const tempDir = fs.realpathSync(temp.mkdirSync()) 564 | const userStylesheetPath = path.join(tempDir, 'styles.less') 565 | fs.writeFileSync(userStylesheetPath, '') 566 | 567 | spyOn(atom.styles, 'getUserStyleSheetPath').andReturn(userStylesheetPath) 568 | 569 | await atom.packages.activatePackage('metrics') 570 | 571 | const editor = await atom.workspace.open(userStylesheetPath) 572 | editor.setText('atom-pane:not(.active) { opacity: 0.75; }') 573 | editor.save() 574 | 575 | await eventReportedPromise({ 576 | 'category': 'customization', 577 | 'action': 'userStylesheetChanged' 578 | }) 579 | }) 580 | }) 581 | 582 | describe('when deactivated', async () => 583 | it('stops reporting pane items', async () => { 584 | global.localStorage.setItem('metrics.userId', 'd') 585 | spyOn(Reporter, 'sendPaneItem') 586 | 587 | const {mainModule} = await atom.packages.activatePackage('metrics') 588 | mainModule.shouldIncludePanesAndCommands = true 589 | await conditionPromise(() => Reporter.addCustomEvent.callCount > 0) 590 | 591 | await atom.workspace.open('file1.txt') 592 | await conditionPromise(() => Reporter.addCustomEvent.callCount > 0) 593 | 594 | Reporter.sendPaneItem.reset() 595 | Reporter.addCustomEvent.reset() 596 | await atom.packages.deactivatePackage('metrics') 597 | await atom.workspace.open('file2.txt') 598 | 599 | expect(Reporter.sendPaneItem.callCount).toBe(0) 600 | expect(Reporter.addCustomEvent.callCount).toBe(0) 601 | }) 602 | ) 603 | 604 | describe('the metrics-reporter service', async () => { 605 | let reporterService = null 606 | beforeEach(async () => { 607 | await atom.packages.activatePackage('metrics').then(pack => { 608 | reporterService = pack.mainModule.provideReporter() 609 | }) 610 | }) 611 | 612 | describe('::sendEvent', () => 613 | it('sends an event', () => { 614 | reporterService.sendEvent('cat', 'action', 'label') 615 | expect(Reporter.addCustomEvent).toHaveBeenCalled() 616 | }) 617 | ) 618 | 619 | describe('::addCustomEvent', () => 620 | it('adds a custom event to the StatsStore', () => { 621 | spyOn(store, 'addCustomEvent') 622 | const args = ['yass queen!', { woo: 'hoo' }] 623 | reporterService.addCustomEvent(...args) 624 | expect(store.addCustomEvent).toHaveBeenCalledWith(...args) 625 | }) 626 | ) 627 | 628 | describe('::incrementCounter', () => 629 | it('increments a counter', () => { 630 | spyOn(store, 'incrementCounter') 631 | const counterName = 'commits' 632 | reporterService.incrementCounter(counterName) 633 | expect(store.incrementCounter).toHaveBeenCalledWith(counterName) 634 | }) 635 | ) 636 | 637 | describe('::addTiming', () => 638 | it('sends timing to StatsStore', () => { 639 | spyOn(store, 'addTiming') 640 | const eventType = 'appStart' 641 | const timingInMilliseconds = 42 642 | const metadata = {glitter: 'beard'} 643 | const args = [eventType, timingInMilliseconds, metadata] 644 | reporterService.addTiming(eventType, timingInMilliseconds, metadata) 645 | expect(store.addTiming).toHaveBeenCalledWith(...args) 646 | }) 647 | ) 648 | 649 | describe('::sendException', () => 650 | it('sends an exception', () => { 651 | reporterService.sendException('desc') 652 | expect(Reporter.addCustomEvent).toHaveBeenCalled() 653 | }) 654 | ) 655 | }) 656 | }) 657 | -------------------------------------------------------------------------------- /spec/repository-helpers-spec.js: -------------------------------------------------------------------------------- 1 | const {getDomain} = require('../lib/repository-helpers') 2 | 3 | describe('getDomain', () => { 4 | it('detects whitelisted domains for HTTPS URLs', () => { 5 | expect(getDomain('https://github.com/electron/node')).toBe('github.com') 6 | expect(getDomain('https://gitlab.com/electron/node')).toBe('gitlab.com') 7 | expect(getDomain('https://bitbucket.org/electron/node')).toBe('bitbucket.org') 8 | expect(getDomain('https://foo.visualstudio.com/electron/node')).toBe('visualstudio.com') 9 | expect(getDomain('https://git-codecommit.us-east-1.amazonaws.com/electron/node')).toBe('amazonaws.com') 10 | }) 11 | 12 | it('returns "other" for non-whitelisted domain in HTTPS URLs', () => { 13 | expect(getDomain('https://example.com/electron/node')).toBe('other') 14 | expect(getDomain('https://localhost/electron/node')).toBe('other') 15 | }) 16 | 17 | it('detects whitelisted domains for SSH URLs', () => { 18 | expect(getDomain('git@github.com:electron/node.git')).toBe('github.com') 19 | expect(getDomain('git@gitlab.com/electron/node')).toBe('gitlab.com') 20 | expect(getDomain('git@bitbucket.org/electron/node')).toBe('bitbucket.org') 21 | expect(getDomain('git@foo.visualstudio.com/electron/node')).toBe('visualstudio.com') 22 | expect(getDomain('git@git-codecommit.us-east-1.amazonaws.com/electron/node')).toBe('amazonaws.com') 23 | }) 24 | 25 | it('returns "other" for non-whitelisted domain in ssh:// URLs', () => { 26 | expect(getDomain('git@example.com:electron/node.git')).toBe('other') 27 | expect(getDomain('git@localhost:electron/node.git')).toBe('other') 28 | }) 29 | 30 | it('returns "other" for local filesystem URLs', () => { 31 | expect(getDomain('electron/node.git')).toBe('other') 32 | expect(getDomain('/srv/git/node.git')).toBe('other') 33 | expect(getDomain('file:///srv/git/node.git')).toBe('other') 34 | expect(getDomain('file:///srv/git/github.com.git')).toBe('other') 35 | }) 36 | 37 | it('detects whitelisted domains for oddball URLs', () => { 38 | expect(getDomain('https://github.com/bitbucket.org/visualstudio.com.gitlab.com')).toBe('github.com') 39 | expect(getDomain('https://bitbucket.org/github.com/visualstudio.com.gitlab.com')).toBe('bitbucket.org') 40 | }) 41 | 42 | it('returns "other" for non-whitelisted domain in oddball URLs', () => { 43 | expect(getDomain('🙀')).toBe('other') 44 | expect(getDomain(null)).toBe('other') 45 | }) 46 | }) 47 | --------------------------------------------------------------------------------