Skip to content

Commit 7c4bc07

Browse files
authored
chore: add query tracking COMPASS-5196 (#2555)
1 parent fa612ac commit 7c4bc07

File tree

14 files changed

+182
-8
lines changed

14 files changed

+182
-8
lines changed

package-lock.json

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/compass-crud/src/components/toolbar.jsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import React from 'react';
22
import PropTypes from 'prop-types';
33
import { ViewSwitcher, Tooltip } from 'hadron-react-components';
44
import { AnimatedIconTextButton, IconButton } from 'hadron-react-buttons';
5+
import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging';
6+
const { track } = createLoggerAndTelemetry('COMPASS-CRUD-UI');
57

68
const BASE_CLASS = 'document-list';
79
const ACTION_BAR_CLASS = `${BASE_CLASS}-action-bar`;
@@ -22,6 +24,7 @@ class Toolbar extends React.Component {
2224
* Handle refreshing the document list.
2325
*/
2426
handleRefreshDocuments() {
27+
track('Query Results Refreshed');
2528
this.props.refreshDocuments();
2629
}
2730

packages/compass-e2e-tests/tests/smoke.test.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,16 +173,28 @@ describe('Smoke tests', function () {
173173
});
174174

175175
it('supports simple find operations', async function () {
176+
const telemetryEntry = await client.listenForTelemetryEvents(telemetry);
176177
await client.runFindOperation('Documents', '{ i: 5 }');
177178

178179
const documentListActionBarMessageElement = await client.$(
179180
Selectors.DocumentListActionBarMessage
180181
);
181182
const text = await documentListActionBarMessageElement.getText();
182183
expect(text).to.equal('Displaying documents 1 - 1 of 1');
184+
const queryExecutedEvent = await telemetryEntry('Query Executed');
185+
expect(queryExecutedEvent).to.deep.equal({
186+
changed_maxtimems: false,
187+
collection_type: 'collection',
188+
has_collation: false,
189+
has_limit: false,
190+
has_projection: false,
191+
has_skip: false,
192+
used_regex: false,
193+
});
183194
});
184195

185196
it('supports advanced find operations', async function () {
197+
const telemetryEntry = await client.listenForTelemetryEvents(telemetry);
186198
await client.runFindOperation('Documents', '{ i: { $gt: 5 } }', {
187199
project: '{ _id: 0 }',
188200
sort: '{ i: -1 }',
@@ -195,6 +207,16 @@ describe('Smoke tests', function () {
195207
);
196208
const text = await documentListActionBarMessageElement.getText();
197209
expect(text).to.equal('Displaying documents 1 - 20 of 50');
210+
const queryExecutedEvent = await telemetryEntry('Query Executed');
211+
expect(queryExecutedEvent).to.deep.equal({
212+
changed_maxtimems: false,
213+
collection_type: 'collection',
214+
has_collation: false,
215+
has_limit: true,
216+
has_projection: true,
217+
has_skip: true,
218+
used_regex: false,
219+
});
198220
});
199221

200222
it('supports cancelling a find and then running another query', async function () {

packages/compass-export-to-language/src/components/export-modal/export-modal.jsx

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import PropTypes from 'prop-types';
77

88
import styles from './export-modal.module.less';
99
import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging';
10+
import { countAggregationStagesInString } from '../../modules/count-aggregation-stages-in-string';
1011
const { track } = createLoggerAndTelemetry('COMPASS-EXPORT-TO-LANGUAGE-UI');
1112

1213
class ExportModal extends PureComponent {
@@ -36,6 +37,9 @@ class ExportModal extends PureComponent {
3637
};
3738

3839
showHandler = () => {
40+
track(this.props.mode === 'Query' ? 'Query Export Opened' : 'Aggregation Export Opened', {
41+
...this.stageCountForTelemetry()
42+
});
3943
track('Screen', { name: 'export_to_language_modal' });
4044
};
4145

@@ -57,6 +61,40 @@ class ExportModal extends PureComponent {
5761
this.props.runTranspiler(this.props.inputExpression);
5862
};
5963

64+
stageCountForTelemetry = () => {
65+
if (this.props.mode === 'Query') {
66+
return {};
67+
}
68+
69+
try {
70+
return {
71+
num_stages: countAggregationStagesInString(this.props.inputExpression.aggregation)
72+
};
73+
} catch (ignore) {
74+
// Things like [{ $match: { x: NumberInt(10) } }] do not evaluate in any kind of context
75+
return { num_stages: -1 };
76+
}
77+
};
78+
79+
copySuccessChanged = (field) => {
80+
if (field === 'output') {
81+
let event;
82+
if (this.props.mode === 'Query') {
83+
event = 'Query Exported';
84+
} else {
85+
event = 'Aggregation Exported';
86+
}
87+
track(event, {
88+
language: this.props.outputLang,
89+
with_import_statements: this.props.showImports,
90+
with_builders: this.props.builders,
91+
with_drivers_syntax: this.props.driver,
92+
...this.stageCountForTelemetry()
93+
});
94+
}
95+
this.props.copySuccessChanged(field);
96+
};
97+
6098
renderBuilderCheckbox = () => {
6199
if (this.props.outputLang === 'java' && this.props.mode === 'Query') {
62100
return (
@@ -90,7 +128,7 @@ class ExportModal extends PureComponent {
90128
</Modal.Header>
91129

92130
<Modal.Body data-test-id="export-to-lang-modal-body">
93-
<ExportForm {...this.props} from={this.props.mode === 'Query' ? this.props.inputExpression.filter : this.props.inputExpression.aggregation}/>
131+
<ExportForm {...this.props} copySuccessChanged={this.copySuccessChanged} from={this.props.mode === 'Query' ? this.props.inputExpression.filter : this.props.inputExpression.aggregation}/>
94132
<div className={classnames(styles['export-to-lang-modal-checkbox-imports'])}>
95133
<Checkbox data-test-id="export-to-lang-checkbox-imports" onClick={this.importsHandler} defaultChecked={this.props.showImports}>
96134
Include Import Statements
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import vm from 'vm';
2+
3+
let dummySandbox;
4+
5+
// We do want to report the number of stages in an aggregation
6+
// for telemetry, but we only receive them as a string from
7+
// the consumers of this package. Instead, we evaluate
8+
// the code in a dummy sandbox environment and just count
9+
// the number of stages in the result. This is a little inefficient,
10+
// but ultimately a simpler solution than pulling in a query
11+
// parser here.
12+
export function countAggregationStagesInString(str) {
13+
if (!dummySandbox) {
14+
dummySandbox = vm.createContext(Object.create(null), {
15+
codeGeneration: { strings: false, wasm: false },
16+
microtaskMode: 'afterEvaluate'
17+
});
18+
vm.runInContext([
19+
'BSONRegExp', 'DBRef', 'Decimal128', 'Double', 'Int32',
20+
'Long', 'Int64', 'MaxKey', 'MinKey', 'ObjectID', 'ObjectId',
21+
'BSONSymbol', 'Timestamp', 'Code', 'Buffer', 'Binary'
22+
].map(name => `function ${name}() {}`).join('\n'), dummySandbox);
23+
}
24+
25+
return vm.runInContext(
26+
'(' + str + ')',
27+
dummySandbox,
28+
{ timeout: 100 }).length;
29+
}

packages/compass-query-bar/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@
148148
"xvfb-maybe": "^0.2.1"
149149
},
150150
"dependencies": {
151+
"@mongodb-js/compass-logging": "^0.3.0",
151152
"lodash": "^4.17.15",
152153
"mongodb-query-util": "^0.2.1"
153154
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export function objectContainsRegularExpression(obj) {
2+
// This assumes that the input is not circular.
3+
if (obj === null || typeof obj !== 'object') {
4+
return false;
5+
}
6+
if (Object.prototype.toString.call(obj) === '[object RegExp]') {
7+
return true;
8+
}
9+
return Object.values(obj).some(objectContainsRegularExpression);
10+
}

packages/compass-query-bar/src/stores/query-bar-store.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
import { bsonEqual, hasDistinctValue } from 'mongodb-query-util';
2929
import QUERY_PROPERTIES from '../constants/query-properties';
3030
import mergeGeoFilter from '../modules/merge-geo-filter';
31+
import { objectContainsRegularExpression } from '../modules/utils';
3132
import {
3233
USER_TYPING_DEBOUNCE_MS,
3334
APPLY_STATE,
@@ -44,11 +45,11 @@ import {
4445
} from '../constants/query-bar-store';
4546
import configureQueryChangedStore from './query-changed-store';
4647

47-
const debug = require('debug')('mongodb-compass:stores:query-bar');
48+
import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging';
49+
const { track, debug } = createLoggerAndTelemetry('COMPASS-QUERY-BAR-UI');
4850

4951
const QUERY_CHANGED_STORE = 'Query.ChangedStore';
5052

51-
5253
/**
5354
* Configure the query bar store.
5455
*
@@ -64,10 +65,11 @@ const configureStore = (options = {}) => {
6465
/*
6566
* listen to Namespace store and reset if ns changes.
6667
*/
67-
onCollectionChanged(ns, isTimeSeries) {
68+
onCollectionChanged(ns, isTimeSeries, isReadonly) {
6869
const newState = this.getInitialState();
6970
newState.ns = ns;
7071
newState.isTimeSeries = isTimeSeries;
72+
newState.isReadonly = isReadonly;
7173
this.setState(newState);
7274
},
7375

@@ -166,6 +168,7 @@ const configureStore = (options = {}) => {
166168
// set the namespace
167169
ns: '',
168170
isTimeSeries: false,
171+
isReadonly: false,
169172

170173
serverVersion: '3.6.0',
171174

@@ -660,6 +663,15 @@ const configureStore = (options = {}) => {
660663
*/
661664
apply() {
662665
if (this._validateQuery()) {
666+
track('Query Executed', {
667+
has_projection: !!this.state.project && Object.keys(this.state.project).length > 0,
668+
has_skip: this.state.skip > 0,
669+
has_limit: this.state.limit > 0,
670+
has_collation: !!this.state.collation,
671+
changed_maxtimems: this.state.maxTimeMS !== DEFAULT_MAX_TIME_MS,
672+
collection_type: this.state.isTimeSeries ? 'time-series' : this.state.isReadonly ? 'readonly' : 'collection',
673+
used_regex: objectContainsRegularExpression(this.state.filter)
674+
});
663675
const registry = this.localAppRegistry;
664676
if (registry) {
665677
const newState = {
@@ -710,6 +722,8 @@ const configureStore = (options = {}) => {
710722
if (this.state.valid) {
711723
const newState = this.getInitialState();
712724
newState.ns = this.state.ns;
725+
newState.isTimeSeries = this.state.isTimeSeries;
726+
newState.isReadonly = this.state.isReadonly;
713727
newState.autoPopulated = true;
714728
this.setState(omit(newState, 'expanded'));
715729
}
@@ -755,7 +769,7 @@ const configureStore = (options = {}) => {
755769
}
756770

757771
if (options.namespace) {
758-
store.onCollectionChanged(options.namespace, options.isTimeSeries);
772+
store.onCollectionChanged(options.namespace, options.isTimeSeries, options.isReadonly);
759773
}
760774

761775
if (options.serverVersion) {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { objectContainsRegularExpression } from '../../src/modules/utils';
2+
3+
describe('objectContainsRegularExpression', () => {
4+
it('tells whether an object contains a regular expression', function() {
5+
expect(objectContainsRegularExpression(null)).to.equal(false);
6+
expect(objectContainsRegularExpression(undefined)).to.equal(false);
7+
expect(objectContainsRegularExpression(1)).to.equal(false);
8+
expect(objectContainsRegularExpression('str')).to.equal(false);
9+
expect(objectContainsRegularExpression({})).to.equal(false);
10+
expect(objectContainsRegularExpression({ x: 1 })).to.equal(false);
11+
expect(objectContainsRegularExpression({ x: /re/ })).to.equal(true);
12+
});
13+
});

packages/compass-query-bar/test/renderer/query-bar-store.spec.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ describe('QueryBarStore [Store]', function() {
6767
expanded: false,
6868
ns: '',
6969
isTimeSeries: false,
70+
isReadonly: false,
7071
schemaFields: []
7172
});
7273
});

0 commit comments

Comments
 (0)