UNPKG

8.54 kBJavaScriptView Raw
1"use strict";
2
3var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
5var _objectWithoutPropertiesLoose2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutPropertiesLoose"));
6
7var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
8
9const uuidv4 = require(`uuid/v4`);
10
11const EventStorage = require(`./event-storage`);
12
13const {
14 cleanPaths
15} = require(`./error-helpers`);
16
17const ci = require(`ci-info`);
18
19const os = require(`os`);
20
21const {
22 join,
23 sep
24} = require(`path`);
25
26const isDocker = require(`is-docker`);
27
28const showAnalyticsNotification = require(`./showAnalyticsNotification`);
29
30const lodash = require(`lodash`);
31
32const {
33 getRepositoryId
34} = require(`./repository-id`);
35
36module.exports = class AnalyticsTracker {
37 // lazy
38 // lazy
39 constructor() {
40 (0, _defineProperty2.default)(this, "store", new EventStorage());
41 (0, _defineProperty2.default)(this, "debouncer", {});
42 (0, _defineProperty2.default)(this, "metadataCache", {});
43 (0, _defineProperty2.default)(this, "defaultTags", {});
44 (0, _defineProperty2.default)(this, "osInfo", void 0);
45 (0, _defineProperty2.default)(this, "trackingEnabled", void 0);
46 (0, _defineProperty2.default)(this, "componentVersion", void 0);
47 (0, _defineProperty2.default)(this, "sessionId", this.getSessionId());
48
49 try {
50 if (this.store.isTrackingDisabled()) {
51 this.trackingEnabled = false;
52 }
53
54 this.defaultTags = this.getTagsFromEnv(); // These may throw and should be last
55
56 this.componentVersion = require(`../package.json`).version;
57 this.gatsbyCliVersion = this.getGatsbyCliVersion();
58 this.installedGatsbyVersion = this.getGatsbyVersion();
59 } catch (e) {// ignore
60 }
61 } // We might have two instances of this lib loaded, one from globally installed gatsby-cli and one from local gatsby.
62 // Hence we need to use process level globals that are not scoped to this module
63
64
65 getSessionId() {
66 return process.gatsbyTelemetrySessionId || (process.gatsbyTelemetrySessionId = uuidv4());
67 }
68
69 getTagsFromEnv() {
70 if (process.env.GATSBY_TELEMETRY_TAGS) {
71 try {
72 return JSON.parse(process.env.GATSBY_TELEMETRY_TAGS);
73 } catch (_) {// ignore
74 }
75 }
76
77 return {};
78 }
79
80 getGatsbyVersion() {
81 const packageInfo = require(join(process.cwd(), `node_modules`, `gatsby`, `package.json`));
82
83 try {
84 return packageInfo.version;
85 } catch (e) {// ignore
86 }
87
88 return undefined;
89 }
90
91 getGatsbyCliVersion() {
92 try {
93 const jsonfile = join(require.resolve(`gatsby-cli`) // Resolve where current gatsby-cli would be loaded from.
94 .split(sep).slice(0, -2) // drop lib/index.js
95 .join(sep), `package.json`);
96
97 const {
98 version
99 } = require(jsonfile).version;
100
101 return version;
102 } catch (e) {// ignore
103 }
104
105 return undefined;
106 }
107
108 captureEvent(type = ``, tags = {}, opts = {
109 debounce: false
110 }) {
111 if (!this.isTrackingEnabled()) {
112 return;
113 }
114
115 let baseEventType = `CLI_COMMAND`;
116
117 if (Array.isArray(type)) {
118 type = type.length > 2 ? type[2].toUpperCase() : ``;
119 baseEventType = `CLI_RAW_COMMAND`;
120 }
121
122 const decoration = this.metadataCache[type];
123 const eventType = `${baseEventType}_${type}`;
124
125 if (opts.debounce) {
126 const debounceTime = 5 * 1000;
127 const now = Date.now();
128 const debounceKey = JSON.stringify({
129 type,
130 decoration,
131 tags
132 });
133 const last = this.debouncer[debounceKey] || 0;
134
135 if (now - last < debounceTime) {
136 return;
137 }
138
139 this.debouncer[debounceKey] = now;
140 }
141
142 delete this.metadataCache[type];
143 this.buildAndStoreEvent(eventType, lodash.merge({}, tags, decoration));
144 }
145
146 captureError(type, tags = {}) {
147 if (!this.isTrackingEnabled()) {
148 return;
149 }
150
151 const decoration = this.metadataCache[type];
152 delete this.metadataCache[type];
153 const eventType = `CLI_ERROR_${type}`;
154 this.formatErrorAndStoreEvent(eventType, lodash.merge({}, tags, decoration));
155 }
156
157 captureBuildError(type, tags = {}) {
158 if (!this.isTrackingEnabled()) {
159 return;
160 }
161
162 const decoration = this.metadataCache[type];
163 delete this.metadataCache[type];
164 const eventType = `BUILD_ERROR_${type}`;
165 this.formatErrorAndStoreEvent(eventType, lodash.merge({}, tags, decoration));
166 }
167
168 formatErrorAndStoreEvent(eventType, tags) {
169 if (tags.error) {
170 var _tags$error, _tags$error2, _tags$error2$error, _tags$error3;
171
172 // `error` ought to have been `errors` but is `error` in the database
173 if (Array.isArray(tags.error)) {
174 const {
175 error
176 } = tags,
177 restOfTags = (0, _objectWithoutPropertiesLoose2.default)(tags, ["error"]);
178 error.forEach(err => {
179 this.formatErrorAndStoreEvent(eventType, Object.assign({
180 error: err
181 }, restOfTags));
182 });
183 return;
184 }
185
186 tags.errorV2 = {
187 id: tags.error.id,
188 text: cleanPaths(tags.error.text),
189 level: tags.error.level,
190 type: (_tags$error = tags.error) === null || _tags$error === void 0 ? void 0 : _tags$error.type,
191 // see if we need empty string or can just use NULL
192 stack: cleanPaths(((_tags$error2 = tags.error) === null || _tags$error2 === void 0 ? void 0 : (_tags$error2$error = _tags$error2.error) === null || _tags$error2$error === void 0 ? void 0 : _tags$error2$error.stack) || ``),
193 context: cleanPaths(JSON.stringify((_tags$error3 = tags.error) === null || _tags$error3 === void 0 ? void 0 : _tags$error3.context))
194 };
195 delete tags.error;
196 }
197
198 this.buildAndStoreEvent(eventType, tags);
199 }
200
201 buildAndStoreEvent(eventType, tags) {
202 const event = Object.assign({
203 installedGatsbyVersion: this.installedGatsbyVersion,
204 gatsbyCliVersion: this.gatsbyCliVersion
205 }, lodash.merge({}, this.defaultTags, tags), {
206 // The schema must include these
207 eventType,
208 sessionId: this.sessionId,
209 time: new Date(),
210 machineId: this.getMachineId(),
211 componentId: `gatsby-cli`,
212 osInformation: this.getOsInfo(),
213 componentVersion: this.componentVersion
214 }, getRepositoryId());
215 this.store.addEvent(event);
216 }
217
218 getMachineId() {
219 // Cache the result
220 if (this.machineId) {
221 return this.machineId;
222 }
223
224 let machineId = this.store.getConfig(`telemetry.machineId`);
225
226 if (!machineId) {
227 machineId = uuidv4();
228 this.store.updateConfig(`telemetry.machineId`, machineId);
229 }
230
231 this.machineId = machineId;
232 return machineId;
233 }
234
235 isTrackingEnabled() {
236 // Cache the result
237 if (this.trackingEnabled !== undefined) {
238 return this.trackingEnabled;
239 }
240
241 let enabled = this.store.getConfig(`telemetry.enabled`);
242
243 if (enabled === undefined || enabled === null) {
244 if (!ci.isCI) {
245 showAnalyticsNotification();
246 }
247
248 enabled = true;
249 this.store.updateConfig(`telemetry.enabled`, enabled);
250 }
251
252 this.trackingEnabled = enabled;
253 return enabled;
254 }
255
256 getOsInfo() {
257 if (this.osInfo) {
258 return this.osInfo;
259 }
260
261 const cpus = os.cpus();
262 const osInfo = {
263 nodeVersion: process.version,
264 platform: os.platform(),
265 release: os.release(),
266 cpus: cpus && cpus.length > 0 && cpus[0].model || undefined,
267 arch: os.arch(),
268 ci: ci.isCI,
269 ciName: ci.isCI && ci.name || process.env.CI_NAME || undefined,
270 docker: isDocker()
271 };
272 this.osInfo = osInfo;
273 return osInfo;
274 }
275
276 trackActivity(source) {
277 if (!this.isTrackingEnabled()) {
278 return;
279 } // debounce by sending only the first event whithin a rolling window
280
281
282 const now = Date.now();
283 const last = this.debouncer[source] || 0;
284 const debounceTime = 5 * 1000; // 5 sec
285
286 if (now - last > debounceTime) {
287 this.captureEvent(source);
288 }
289
290 this.debouncer[source] = now;
291 }
292
293 decorateNextEvent(event, obj) {
294 const cached = this.metadataCache[event] || {};
295 this.metadataCache[event] = Object.assign(cached, obj);
296 }
297
298 decorateAll(tags) {
299 this.defaultTags = Object.assign(this.defaultTags, tags);
300 }
301
302 setTelemetryEnabled(enabled) {
303 this.trackingEnabled = enabled;
304 this.store.updateConfig(`telemetry.enabled`, enabled);
305 }
306
307 async sendEvents() {
308 if (!this.isTrackingEnabled()) {
309 return Promise.resolve();
310 }
311
312 return this.store.sendEvents();
313 }
314
315};
\No newline at end of file