UNPKG

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