Integrate metrics #1562
Integrate metrics #1562
Changes from 17 commits
4ddf13f
bd887ea
7c66926
b46cf3c
a0a9aaf
eef8a7a
3d8d723
71d1e35
e2476f6
6510946
6b9ceb3
41c2cfa
4cef44b
b0e3529
2d8d3e5
4840688
8e2c894
26748e4
c099af0
65c3a03
c036656
2a3f244
7a8dbad
348ec0d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,7 @@ import {parse as parseStatus} from 'what-the-status'; | |
import GitPromptServer from './git-prompt-server'; | ||
import GitTempDir from './git-temp-dir'; | ||
import AsyncQueue from './async-queue'; | ||
import {incrementCounter} from './reporter-proxy'; | ||
import { | ||
getDugitePath, getSharedModulePath, getAtomHelperPath, | ||
extractCoAuthorsAndRawCommitMessage, fileExists, isFileExecutable, isFileSymlink, isBinary, | ||
|
@@ -42,6 +43,9 @@ export class LargeRepoError extends Error { | |
} | ||
} | ||
|
||
// ignored for the purposes of usage metrics tracking because they're noisy | ||
const IGNORED_GIT_COMMANDS = ['cat-file', 'config', 'diff', 'for-each-ref', 'log', 'rev-parse', 'status']; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be more reliable to list the commands we do want to capture? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good question. My thinking on this was that
|
||
|
||
const DISABLE_COLOR_FLAGS = [ | ||
'branch', 'diff', 'showBranch', 'status', 'ui', | ||
].reduce((acc, type) => { | ||
|
@@ -89,6 +93,7 @@ export default class GitShellOutStrategy { | |
async exec(args, options = GitShellOutStrategy.defaultExecArgs) { | ||
/* eslint-disable no-console,no-control-regex */ | ||
const {stdin, useGitPromptServer, useGpgWrapper, useGpgAtomPrompt, writeOperation} = options; | ||
const commandName = args[0]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
const subscriptions = new CompositeDisposable(); | ||
const diagnosticsEnabled = process.env.ATOM_GITHUB_GIT_DIAGNOSTICS || atom.config.get('github.gitDiagnostics'); | ||
|
||
|
@@ -311,6 +316,10 @@ export default class GitShellOutStrategy { | |
err.command = formattedArgs; | ||
reject(err); | ||
} | ||
|
||
if (!IGNORED_GIT_COMMANDS.includes(commandName)) { | ||
incrementCounter(commandName); | ||
} | ||
resolve(stdout); | ||
}); | ||
}, {parallel: !writeOperation}); | ||
|
@@ -473,7 +482,7 @@ export default class GitShellOutStrategy { | |
|
||
if (amend) { args.push('--amend'); } | ||
if (allowEmpty) { args.push('--allow-empty'); } | ||
return this.gpgExec(args, {writeOperation: true}); | ||
return await this.gpgExec(args, {writeOperation: true}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ☝️ You shouldn't need this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 oops. fixed. |
||
} | ||
|
||
addCoAuthorsToMessage(message, coAuthors = []) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
const pjson = require('../package.json'); | ||
|
||
// this class allows us to call reporter methods | ||
// before the reporter is actually loaded, since we don't want to | ||
// assume that the metrics package will load before the GitHub package. | ||
|
||
// maybe this should be an object instead of a class. | ||
// IDK. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Haha, this is fine. One advantage of having a class - you can export it and test its internals independent of the global state. |
||
class ReporterProxy { | ||
constructor() { | ||
this.reporter = null; | ||
this.events = []; | ||
this.timings = []; | ||
this.counters = []; | ||
this.gitHubPackageVersion = pjson.version; | ||
} | ||
|
||
// function that is called after the reporter is actually loaded, to | ||
// set the reporter and send any data that have accumulated while it was loading. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One question: what happens if the metrics package is disabled and this is never called? It'd be good not to endlessly accumulate events and slowly eat RAM without bound... but I'm not sure how we could know that this method won't be called. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh, gooooooood question. Hadn't thought about that edge case. I guess I could set a timer, and if the metrics package fails to be activated within a reasonable timeframe, cast the events into the void? I can't think of a better way to handle this but I'm open to suggestions. Do you know how common it is for users to straight up disable core packages like that? Now I'm curious. |
||
setReporter(reporter) { | ||
this.reporter = reporter; | ||
|
||
this.events.forEach(customEvent => { | ||
this.reporter.addCustomEvent(customEvent.eventType, customEvent.event); | ||
}); | ||
|
||
this.timings.forEach(timing => { | ||
this.reporter.addTiming(timing.eventType, timing.durationInMilliseconds, timing.metadata); | ||
}); | ||
|
||
this.counters.forEach(counterName => { | ||
this.reporter.incrementCounter(counterName); | ||
}); | ||
} | ||
} | ||
|
||
export const reporterProxy = new ReporterProxy(); | ||
|
||
export const incrementCounter = function(counterName) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any particular reason for the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wasn't sure if |
||
if (reporterProxy.reporter) { | ||
reporterProxy.reporter.incrementCounter(counterName); | ||
} else { | ||
reporterProxy.counters.push(counterName); | ||
} | ||
}; | ||
|
||
export const addTiming = function(eventType, durationInMilliseconds, metadata = {}) { | ||
metadata.gitHubPackageVersion = reporterProxy.gitHubPackageVersion; | ||
if (reporterProxy.reporter) { | ||
reporterProxy.reporter.addTiming(eventType, durationInMilliseconds, metadata); | ||
} else { | ||
reporterProxy.timings.push({eventType, durationInMilliseconds, metadata}); | ||
} | ||
}; | ||
|
||
export const addEvent = function(eventType, event) { | ||
event.gitHubPackageVersion = reporterProxy.gitHubPackageVersion; | ||
if (reporterProxy.reporter) { | ||
reporterProxy.reporter.addCustomEvent(eventType, event); | ||
} else { | ||
reporterProxy.events.push({eventType, event}); | ||
} | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
import React from 'react'; | ||
import {mount} from 'enzyme'; | ||
|
||
import * as reporterProxy from '../../lib/reporter-proxy'; | ||
import {createRepositoryResult} from '../fixtures/factories/repository-result'; | ||
import Remote from '../../lib/models/remote'; | ||
import Branch, {nullBranch} from '../../lib/models/branch'; | ||
|
@@ -131,6 +132,28 @@ describe('RemoteContainer', function() { | |
assert.strictEqual(qev.prop('error'), e); | ||
}); | ||
|
||
it('increments a counter on login', function() { | ||
const token = '1234'; | ||
sinon.stub(model, 'getScopes').resolves(GithubLoginModel.REQUIRED_SCOPES); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm I should probably add this as a helper or something - you have to stub it every time any test needs to touch the |
||
const incrementCounterStub = sinon.stub(reporterProxy, 'incrementCounter'); | ||
|
||
const wrapper = mount(buildApp()); | ||
wrapper.instance().handleLogin(token); | ||
assert.isTrue(incrementCounterStub.calledOnceWith('github-login')); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reminder to self: look up how to set up Sinon with Chai expectations again so we can (hopefully) get more informative failure messages on these. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. jah, seems like a good issue for the cleanup sprint. |
||
}); | ||
|
||
it('increments a counter on logout', function() { | ||
const token = '1234'; | ||
sinon.stub(model, 'getScopes').resolves(GithubLoginModel.REQUIRED_SCOPES); | ||
|
||
const wrapper = mount(buildApp()); | ||
wrapper.instance().handleLogin(token); | ||
|
||
const incrementCounterStub = sinon.stub(reporterProxy, 'incrementCounter'); | ||
wrapper.instance().handleLogout(); | ||
assert.isTrue(incrementCounterStub.calledOnceWith('github-logout')); | ||
}); | ||
|
||
it('renders the controller once results have arrived', async function() { | ||
const {resolve} = expectRepositoryQuery(); | ||
expectEmptyIssueishQuery(); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,13 @@ | ||
import React from 'react'; | ||
import {shallow} from 'enzyme'; | ||
import {shell} from 'electron'; | ||
|
||
import BranchSet from '../../lib/models/branch-set'; | ||
import Branch, {nullBranch} from '../../lib/models/branch'; | ||
import Remote from '../../lib/models/remote'; | ||
import {nullOperationStateObserver} from '../../lib/models/operation-state-observer'; | ||
import RemoteController from '../../lib/controllers/remote-controller'; | ||
import * as reporterProxy from '../../lib/reporter-proxy'; | ||
|
||
describe('RemoteController', function() { | ||
let atomEnv, remote, branchSet, currentBranch; | ||
|
@@ -49,6 +51,29 @@ describe('RemoteController', function() { | |
); | ||
} | ||
|
||
it('increments a counter when onCreatePr is called', async function() { | ||
const wrapper = shallow(createApp()); | ||
sinon.stub(shell, 'openExternal').resolves(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, this is the bit where you were having trouble getting the Promise to resolve? Because the real sinon.stub(shell, 'openExternal').callsArg(2); And to report an error: sinon.stub(shell, 'openExternal').callsArgWith(2, new Error('boom')); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah thanks, I'll try that! |
||
sinon.stub(reporterProxy, 'incrementCounter'); | ||
|
||
await wrapper.instance().onCreatePr(); | ||
assert.equal(reporterProxy.incrementCounter.callCount, 1); | ||
assert.deepEqual(reporterProxy.incrementCounter.lastCall.args, ['create-pull-request']); | ||
}); | ||
|
||
it('handles error when onCreatePr fails', async function() { | ||
const wrapper = shallow(createApp()); | ||
sinon.stub(shell, 'openExternal').rejects(new Error('oh noes')); | ||
sinon.stub(reporterProxy, 'incrementCounter'); | ||
|
||
try { | ||
await wrapper.instance().onCreatePr(); | ||
} catch (err) { | ||
assert.equal(err.message, 'oh noes'); | ||
} | ||
assert.equal(reporterProxy.incrementCounter.callCount, 0); | ||
}); | ||
|
||
it('renders issueish searches', function() { | ||
const wrapper = shallow(createApp()); | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@smashwilson, does this look right to you?
I was having trouble getting the inner promise to return for the test. Other than adding the counter, this should behave identically.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like
openExternal
uses a callback instead of returning a Promise, at least on macOS:(From what I can tell, on other platforms, the callback will be called synchronously instead.)
Try putting the
incrementCounter()
call inside theif
statement instead:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that was the first thing I tried, and the promise never returned in the test.
I also tried using a .done() callback in the test and the promise still did not return.
Lemmesee if I can figure out a different approach.