UNPKG

7.33 kBJavaScriptView Raw
1const uuidv4 = require(`uuid/v4`)
2const EventStorage = require(`./event-storage`)
3const { cleanPaths } = require(`./error-helpers`)
4const ci = require(`ci-info`)
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 id: tags.error.id,
158 text: cleanPaths(tags.error.text),
159 level: tags.error.level,
160 type: tags.error?.type,
161 // see if we need empty string or can just use NULL
162 stack: cleanPaths(tags.error?.error?.stack || ``),
163 context: cleanPaths(JSON.stringify(tags.error?.context)),
164 }
165
166 delete tags.error
167 }
168
169 this.buildAndStoreEvent(eventType, tags)
170 }
171
172 buildAndStoreEvent(eventType, tags) {
173 const event = {
174 installedGatsbyVersion: this.installedGatsbyVersion,
175 gatsbyCliVersion: this.gatsbyCliVersion,
176 ...lodash.merge({}, this.defaultTags, tags), // The schema must include these
177 eventType,
178 sessionId: this.sessionId,
179 time: new Date(),
180 machineId: this.getMachineId(),
181 componentId: `gatsby-cli`,
182 osInformation: this.getOsInfo(),
183 componentVersion: this.componentVersion,
184 ...getRepositoryId(),
185 }
186 this.store.addEvent(event)
187 }
188
189 getMachineId() {
190 // Cache the result
191 if (this.machineId) {
192 return this.machineId
193 }
194 let machineId = this.store.getConfig(`telemetry.machineId`)
195 if (!machineId) {
196 machineId = uuidv4()
197 this.store.updateConfig(`telemetry.machineId`, machineId)
198 }
199 this.machineId = machineId
200 return machineId
201 }
202
203 isTrackingEnabled() {
204 // Cache the result
205 if (this.trackingEnabled !== undefined) {
206 return this.trackingEnabled
207 }
208 let enabled = this.store.getConfig(`telemetry.enabled`)
209 if (enabled === undefined || enabled === null) {
210 if (!ci.isCI) {
211 showAnalyticsNotification()
212 }
213 enabled = true
214 this.store.updateConfig(`telemetry.enabled`, enabled)
215 }
216 this.trackingEnabled = enabled
217 return enabled
218 }
219
220 getOsInfo() {
221 if (this.osInfo) {
222 return this.osInfo
223 }
224 const cpus = os.cpus()
225 const osInfo = {
226 nodeVersion: process.version,
227 platform: os.platform(),
228 release: os.release(),
229 cpus: (cpus && cpus.length > 0 && cpus[0].model) || undefined,
230 arch: os.arch(),
231 ci: ci.isCI,
232 ciName: (ci.isCI && ci.name) || process.env.CI_NAME || undefined,
233 docker: isDocker(),
234 }
235 this.osInfo = osInfo
236 return osInfo
237 }
238
239 trackActivity(source) {
240 if (!this.isTrackingEnabled()) {
241 return
242 }
243 // debounce by sending only the first event whithin a rolling window
244 const now = Date.now()
245 const last = this.debouncer[source] || 0
246 const debounceTime = 5 * 1000 // 5 sec
247
248 if (now - last > debounceTime) {
249 this.captureEvent(source)
250 }
251 this.debouncer[source] = now
252 }
253
254 decorateNextEvent(event, obj) {
255 const cached = this.metadataCache[event] || {}
256 this.metadataCache[event] = Object.assign(cached, obj)
257 }
258
259 decorateAll(tags) {
260 this.defaultTags = Object.assign(this.defaultTags, tags)
261 }
262
263 setTelemetryEnabled(enabled) {
264 this.trackingEnabled = enabled
265 this.store.updateConfig(`telemetry.enabled`, enabled)
266 }
267
268 async sendEvents() {
269 if (!this.isTrackingEnabled()) {
270 return Promise.resolve()
271 }
272 return this.store.sendEvents()
273 }
274}