diff --git a/packages/gatsby-core-utils/src/ci.ts b/packages/gatsby-core-utils/src/ci.ts index 16de566157e80..5dc2231b0a6c7 100644 --- a/packages/gatsby-core-utils/src/ci.ts +++ b/packages/gatsby-core-utils/src/ci.ts @@ -1,7 +1,7 @@ import ci from "ci-info" const CI_DEFINITIONS = [ - envFromCIandCIName, + envFromCIAndCIName, getEnvDetect({ key: `NOW_BUILDER_ANNOTATE`, name: `ZEIT Now` }), getEnvDetect({ key: `NOW_REGION`, name: `ZEIT Now v1` }), herokuDetect, @@ -73,7 +73,7 @@ function herokuDetect(): false | "Heroku" { ) } -function envFromCIandCIName(): string | null { +function envFromCIAndCIName(): string | null { if (process.env.CI_NAME && process.env.CI) { return process.env.CI_NAME } diff --git a/packages/gatsby-telemetry/src/__tests__/telemetry.js b/packages/gatsby-telemetry/src/__tests__/telemetry.ts similarity index 87% rename from packages/gatsby-telemetry/src/__tests__/telemetry.js rename to packages/gatsby-telemetry/src/__tests__/telemetry.ts index c2d5a2ae0b33b..4ccbd6425465d 100644 --- a/packages/gatsby-telemetry/src/__tests__/telemetry.js +++ b/packages/gatsby-telemetry/src/__tests__/telemetry.ts @@ -1,11 +1,11 @@ jest.mock(`../event-storage`) const eventStore = require(`../event-storage`) -const Telemetry = require(`../telemetry`) +import { AnalyticsTracker } from "../telemetry" let telemetry beforeEach(() => { eventStore.mockReset() - telemetry = new Telemetry() + telemetry = new AnalyticsTracker() }) describe(`Telemetry`, () => { diff --git a/packages/gatsby-telemetry/src/declarations.d.ts b/packages/gatsby-telemetry/src/declarations.d.ts new file mode 100644 index 0000000000000..0a5bde3e9a554 --- /dev/null +++ b/packages/gatsby-telemetry/src/declarations.d.ts @@ -0,0 +1,8 @@ +type UUID = string + +declare namespace NodeJS { + interface Process { + gatsbyTelemetrySessionId: UUID; + } +} + diff --git a/packages/gatsby-telemetry/src/index.js b/packages/gatsby-telemetry/src/index.js deleted file mode 100644 index 2ccc3646679f9..0000000000000 --- a/packages/gatsby-telemetry/src/index.js +++ /dev/null @@ -1,42 +0,0 @@ -const Telemetry = require(`./telemetry`) -const instance = new Telemetry() - -const flush = require(`./flush`)(instance.isTrackingEnabled()) - -process.on(`exit`, flush) - -// For long running commands we want to occasionally flush the data -// The data is also sent on exit. - -const interval = Number.isFinite(+process.env.TELEMETRY_BUFFER_INTERVAL) - ? Math.max(Number(process.env.TELEMETRY_BUFFER_INTERVAL), 1000) - : 10 * 60 * 1000 // 10 min - -const tick = _ => { - flush() - .catch(console.error) - .then(_ => setTimeout(tick, interval)) -} - -module.exports = { - trackCli: (input, tags, opts) => instance.captureEvent(input, tags, opts), - trackError: (input, tags) => instance.captureError(input, tags), - trackBuildError: (input, tags) => instance.captureBuildError(input, tags), - setDefaultTags: tags => instance.decorateAll(tags), - decorateEvent: (event, tags) => instance.decorateNextEvent(event, tags), - setTelemetryEnabled: enabled => instance.setTelemetryEnabled(enabled), - startBackgroundUpdate: _ => { - setTimeout(tick, interval) - }, - isTrackingEnabled: () => instance.isTrackingEnabled(), - aggregateStats: data => instance.aggregateStats(data), - addSiteMeasurement: (event, obj) => instance.addSiteMeasurement(event, obj), - expressMiddleware: source => (req, res, next) => { - try { - instance.trackActivity(`${source}_ACTIVE`) - } catch (e) { - // ignore - } - next() - }, -} diff --git a/packages/gatsby-telemetry/src/index.ts b/packages/gatsby-telemetry/src/index.ts new file mode 100644 index 0000000000000..216ca65a6dab4 --- /dev/null +++ b/packages/gatsby-telemetry/src/index.ts @@ -0,0 +1,56 @@ +import { AnalyticsTracker, IAggregateStats } from "./telemetry" +import * as express from "express" + +const instance = new AnalyticsTracker() + +const flush = require(`./flush`)(instance.isTrackingEnabled()) + +process.on(`exit`, flush) + +// For long running commands we want to occasionally flush the data +// +// The data is also sent on exit. + +const intervalDuration = process.env.TELEMETRY_BUFFER_INTERVAL +const interval = + intervalDuration && Number.isFinite(+intervalDuration) + ? Math.max(Number(intervalDuration), 1000) + : 10 * 60 * 1000 // 10 min + +function tick(): void { + flush() + .catch(console.error) + .then(() => setTimeout(tick, interval)) +} + +module.exports = { + trackCli: (input, tags, opts): void => + instance.captureEvent(input, tags, opts), + trackError: (input, tags): void => instance.captureError(input, tags), + trackBuildError: (input, tags): void => + instance.captureBuildError(input, tags), + setDefaultTags: (tags): void => instance.decorateAll(tags), + decorateEvent: (event, tags): void => instance.decorateNextEvent(event, tags), + setTelemetryEnabled: (enabled): void => instance.setTelemetryEnabled(enabled), + startBackgroundUpdate: (): void => { + setTimeout(tick, interval) + }, + isTrackingEnabled: (): boolean => instance.isTrackingEnabled(), + aggregateStats: (data): IAggregateStats => instance.aggregateStats(data), + addSiteMeasurement: (event, obj): void => + instance.addSiteMeasurement(event, obj), + expressMiddleware: function (source: string) { + return function ( + _req: express.Request, + _res: express.Response, + next + ): void { + try { + instance.trackActivity(`${source}_ACTIVE`) + } catch (e) { + // ignore + } + next() + } + }, +} diff --git a/packages/gatsby-telemetry/src/repository-id.ts b/packages/gatsby-telemetry/src/repository-id.ts index c248e6319a26e..703888866ca3b 100644 --- a/packages/gatsby-telemetry/src/repository-id.ts +++ b/packages/gatsby-telemetry/src/repository-id.ts @@ -15,7 +15,7 @@ interface IRepositoryData { name?: string } -interface IRepositoryId { +export interface IRepositoryId { repositoryId: string repositoryData?: IRepositoryData | null } diff --git a/packages/gatsby-telemetry/src/send.js b/packages/gatsby-telemetry/src/send.js deleted file mode 100644 index 87ef881717dc3..0000000000000 --- a/packages/gatsby-telemetry/src/send.js +++ /dev/null @@ -1,10 +0,0 @@ -const Telemetry = require(`./telemetry`) -const instance = new Telemetry() - -const flush = _ => { - instance.sendEvents().catch(e => { - // ignore - }) -} - -flush() diff --git a/packages/gatsby-telemetry/src/send.ts b/packages/gatsby-telemetry/src/send.ts new file mode 100644 index 0000000000000..fd9885460b174 --- /dev/null +++ b/packages/gatsby-telemetry/src/send.ts @@ -0,0 +1,10 @@ +import { AnalyticsTracker } from "./telemetry" +const instance = new AnalyticsTracker() + +function flush(): void { + instance.sendEvents().catch(_e => { + // ignore + }) +} + +flush() diff --git a/packages/gatsby-telemetry/src/showAnalyticsNotification.js b/packages/gatsby-telemetry/src/show-analytics-notification.ts similarity index 56% rename from packages/gatsby-telemetry/src/showAnalyticsNotification.js rename to packages/gatsby-telemetry/src/show-analytics-notification.ts index f6f8b3c1f4aa5..05aa5869487ba 100644 --- a/packages/gatsby-telemetry/src/showAnalyticsNotification.js +++ b/packages/gatsby-telemetry/src/show-analytics-notification.ts @@ -1,10 +1,10 @@ -const boxen = require(`boxen`) +import boxen from "boxen" const defaultConfig = { padding: 1, borderColor: `blue`, borderStyle: `double`, -} +} as boxen.Options const defaultMessage = `Gatsby collects anonymous usage analytics\n` + @@ -15,14 +15,10 @@ const defaultMessage = /** * Analytics notice for the end-user - * @param {Object} config - The configuration that boxen accepts. https://github.com/sindresorhus/boxen#api - * @param {string} message - Message shown to the end-user */ -const showAnalyticsNotification = ( - config = defaultConfig, - message = defaultMessage -) => { +export function showAnalyticsNotification( + config: boxen.Options = defaultConfig, + message: string = defaultMessage +): void { console.log(boxen(message, config)) } - -module.exports = showAnalyticsNotification diff --git a/packages/gatsby-telemetry/src/telemetry.js b/packages/gatsby-telemetry/src/telemetry.ts similarity index 78% rename from packages/gatsby-telemetry/src/telemetry.js rename to packages/gatsby-telemetry/src/telemetry.ts index d88461d7ff2d7..e689f2f7a83b5 100644 --- a/packages/gatsby-telemetry/src/telemetry.js +++ b/packages/gatsby-telemetry/src/telemetry.ts @@ -1,23 +1,61 @@ -const uuidv4 = require(`uuid/v4`) +import uuidV4 from "uuid/v4" +import { isCI, getCIName } from "gatsby-core-utils" + +import { + getRepositoryId as _getRepositoryId, + IRepositoryId, +} from "./repository-id" +import os from "os" + +import { join, sep } from "path" +import isDocker from "is-docker" +import { showAnalyticsNotification } from "./show-analytics-notification" +import lodash from "lodash" + +// TODO convert to TypeScript const EventStorage = require(`./event-storage`) const { cleanPaths } = require(`./error-helpers`) -const { isCI, getCIName } = require(`gatsby-core-utils`) -const os = require(`os`) -const { join, sep } = require(`path`) -const isDocker = require(`is-docker`) -const showAnalyticsNotification = require(`./showAnalyticsNotification`) -const lodash = require(`lodash`) -import { getRepositoryId as _getRepositoryId } from "./repository-id" - -module.exports = class AnalyticsTracker { + +const dbEngine = `redux` + +type UUID = string +type SemVer = string | undefined + +interface IOSInfo { + nodeVersion: SemVer + platform: string + release: string + cpus: string | undefined + arch: string + ci: boolean | undefined + ciName: string | null + docker: boolean | undefined +} + +export interface IAggregateStats { + count: number + min: number + max: number + sum: number + mean: number + median: number + stdDev: number + skewness: number +} + +export class AnalyticsTracker { store = new EventStorage() debouncer = {} metadataCache = {} defaultTags = {} - osInfo // lazy - trackingEnabled // lazy - componentVersion - sessionId = this.getSessionId() + osInfo?: IOSInfo // lazy + trackingEnabled?: boolean // lazy + componentVersion?: string + sessionId: string = this.getSessionId() + gatsbyCliVersion?: SemVer + installedGatsbyVersion?: SemVer + repositoryId?: IRepositoryId + machineId?: UUID constructor() { try { @@ -38,21 +76,21 @@ module.exports = class AnalyticsTracker { // We might have two instances of this lib loaded, one from globally installed gatsby-cli and one from local gatsby. // Hence we need to use process level globals that are not scoped to this module - getSessionId() { + getSessionId(): UUID { return ( process.gatsbyTelemetrySessionId || - (process.gatsbyTelemetrySessionId = uuidv4()) + (process.gatsbyTelemetrySessionId = uuidV4()) ) } - getRepositoryId() { + getRepositoryId(): IRepositoryId { if (!this.repositoryId) { this.repositoryId = _getRepositoryId() } return this.repositoryId } - getTagsFromEnv() { + getTagsFromEnv(): Record { if (process.env.GATSBY_TELEMETRY_TAGS) { try { return JSON.parse(process.env.GATSBY_TELEMETRY_TAGS) @@ -63,7 +101,7 @@ module.exports = class AnalyticsTracker { return {} } - getGatsbyVersion() { + getGatsbyVersion(): SemVer { const packageInfo = require(join( process.cwd(), `node_modules`, @@ -78,7 +116,7 @@ module.exports = class AnalyticsTracker { return undefined } - getGatsbyCliVersion() { + getGatsbyCliVersion(): SemVer { try { const jsonfile = join( require @@ -95,7 +133,7 @@ module.exports = class AnalyticsTracker { } return undefined } - captureEvent(type = ``, tags = {}, opts = { debounce: false }) { + captureEvent(type = ``, tags = {}, opts = { debounce: false }): void { if (!this.isTrackingEnabled()) { return } @@ -123,7 +161,7 @@ module.exports = class AnalyticsTracker { this.buildAndStoreEvent(eventType, lodash.merge({}, tags, decoration)) } - captureError(type, tags = {}) { + captureError(type, tags = {}): void { if (!this.isTrackingEnabled()) { return } @@ -135,7 +173,7 @@ module.exports = class AnalyticsTracker { this.formatErrorAndStoreEvent(eventType, lodash.merge({}, tags, decoration)) } - captureBuildError(type, tags = {}) { + captureBuildError(type, tags = {}): void { if (!this.isTrackingEnabled()) { return } @@ -146,7 +184,7 @@ module.exports = class AnalyticsTracker { this.formatErrorAndStoreEvent(eventType, lodash.merge({}, tags, decoration)) } - formatErrorAndStoreEvent(eventType, tags) { + formatErrorAndStoreEvent(eventType, tags): void { if (tags.error) { // `error` ought to have been `errors` but is `error` in the database if (Array.isArray(tags.error)) { @@ -177,7 +215,7 @@ module.exports = class AnalyticsTracker { this.buildAndStoreEvent(eventType, tags) } - buildAndStoreEvent(eventType, tags) { + buildAndStoreEvent(eventType, tags): void { const event = { installedGatsbyVersion: this.installedGatsbyVersion, gatsbyCliVersion: this.gatsbyCliVersion, @@ -189,31 +227,27 @@ module.exports = class AnalyticsTracker { componentId: `gatsby-cli`, osInformation: this.getOsInfo(), componentVersion: this.componentVersion, - dbEngine: this.getDbEngine(), + dbEngine, ...this.getRepositoryId(), } this.store.addEvent(event) } - getDbEngine() { - return `redux` - } - - getMachineId() { + getMachineId(): UUID { // Cache the result if (this.machineId) { return this.machineId } let machineId = this.store.getConfig(`telemetry.machineId`) if (!machineId) { - machineId = uuidv4() + machineId = uuidV4() this.store.updateConfig(`telemetry.machineId`, machineId) } this.machineId = machineId return machineId } - isTrackingEnabled() { + isTrackingEnabled(): boolean { // Cache the result if (this.trackingEnabled !== undefined) { return this.trackingEnabled @@ -230,7 +264,7 @@ module.exports = class AnalyticsTracker { return enabled } - getOsInfo() { + getOsInfo(): IOSInfo { if (this.osInfo) { return this.osInfo } @@ -249,11 +283,11 @@ module.exports = class AnalyticsTracker { return osInfo } - trackActivity(source) { + trackActivity(source): void { if (!this.isTrackingEnabled()) { return } - // debounce by sending only the first event whithin a rolling window + // debounce by sending only the first event within a rolling window const now = Date.now() const last = this.debouncer[source] || 0 const debounceTime = 5 * 1000 // 5 sec @@ -264,12 +298,12 @@ module.exports = class AnalyticsTracker { this.debouncer[source] = now } - decorateNextEvent(event, obj) { + decorateNextEvent(event, obj): void { const cached = this.metadataCache[event] || {} this.metadataCache[event] = Object.assign(cached, obj) } - addSiteMeasurement(event, obj) { + addSiteMeasurement(event, obj): void { const cachedEvent = this.metadataCache[event] || {} const cachedMeasurements = cachedEvent.siteMeasurements || {} this.metadataCache[event] = Object.assign(cachedEvent, { @@ -277,16 +311,16 @@ module.exports = class AnalyticsTracker { }) } - decorateAll(tags) { + decorateAll(tags): void { this.defaultTags = Object.assign(this.defaultTags, tags) } - setTelemetryEnabled(enabled) { + setTelemetryEnabled(enabled: boolean): void { this.trackingEnabled = enabled this.store.updateConfig(`telemetry.enabled`, enabled) } - aggregateStats(data) { + aggregateStats(data): IAggregateStats { const sum = data.reduce((acc, x) => acc + x, 0) const mean = sum / data.length || 0 const median = data.sort()[Math.floor((data.length - 1) / 2)] || 0 @@ -313,9 +347,9 @@ module.exports = class AnalyticsTracker { } } - async sendEvents() { + async sendEvents(): Promise { if (!this.isTrackingEnabled()) { - return Promise.resolve() + return Promise.resolve(true) } return this.store.sendEvents()