diff --git a/packages/angular_devkit/core/src/analytics/api.ts b/packages/angular_devkit/core/src/analytics/api.ts new file mode 100644 index 0000000000..f538d1ba08 --- /dev/null +++ b/packages/angular_devkit/core/src/analytics/api.ts @@ -0,0 +1,52 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +export interface CustomDimensionsAndMetricsOptions { + dimensions?: (boolean | number | string)[]; + metrics?: (boolean | number | string)[]; +} + +export interface EventOptions extends CustomDimensionsAndMetricsOptions { + label?: string; + value?: string; +} + +export interface ScreenviewOptions extends CustomDimensionsAndMetricsOptions { + appVersion?: string; + appId?: string; + appInstallerId?: string; +} + +export interface PageviewOptions extends CustomDimensionsAndMetricsOptions { + hostname?: string; + title?: string; +} + +export interface TimingOptions extends CustomDimensionsAndMetricsOptions { + label?: string; +} + +/** + * Interface for managing analytics. This is highly platform dependent, and mostly matches + * Google Analytics. The reason the interface is here is to remove the dependency to an + * implementation from most other places. + * + * The methods exported from this interface more or less match those needed by us in the + * universal analytics package, see https://unpkg.com/@types/universal-analytics@0.4.2/index.d.ts + * for typings. We mostly named arguments to make it easier to follow, but didn't change or + * add any semantics to those methods. They're mapping GA and u-a one for one. + * + * The Angular CLI (or any other kind of backend) should forward it to some compatible backend. + */ +export interface Analytics { + event(category: string, action: string, options?: EventOptions): void; + screenview(screenName: string, appName: string, options?: ScreenviewOptions): void; + pageview(path: string, options?: PageviewOptions): void; + timing(category: string, variable: string, time: string | number, options?: TimingOptions): void; + + flush(): Promise; +} diff --git a/packages/angular_devkit/core/src/analytics/forwarder.ts b/packages/angular_devkit/core/src/analytics/forwarder.ts new file mode 100644 index 0000000000..8c1ba6de6a --- /dev/null +++ b/packages/angular_devkit/core/src/analytics/forwarder.ts @@ -0,0 +1,131 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { JsonObject } from '../json'; +import { Analytics, EventOptions, PageviewOptions, ScreenviewOptions, TimingOptions } from './api'; + + +export enum AnalyticsReportKind { + Event = 'event', + Screenview = 'screenview', + Pageview = 'pageview', + Timing = 'timing', +} + +export interface AnalyticsReportBase extends JsonObject { + kind: AnalyticsReportKind; +} + +export interface AnalyticsReportEvent extends AnalyticsReportBase { + kind: AnalyticsReportKind.Event; + options: JsonObject & EventOptions; + category: string; + action: string; +} +export interface AnalyticsReportScreenview extends AnalyticsReportBase { + kind: AnalyticsReportKind.Screenview; + options: JsonObject & ScreenviewOptions; + screenName: string; + appName: string; +} +export interface AnalyticsReportPageview extends AnalyticsReportBase { + kind: AnalyticsReportKind.Pageview; + options: JsonObject & PageviewOptions; + path: string; +} +export interface AnalyticsReportTiming extends AnalyticsReportBase { + kind: AnalyticsReportKind.Timing; + options: JsonObject & TimingOptions; + category: string; + variable: string; + time: string | number; +} + +export type AnalyticsReport = + AnalyticsReportEvent + | AnalyticsReportScreenview + | AnalyticsReportPageview + | AnalyticsReportTiming + ; + +/** + * A function that can forward analytics along some stream. AnalyticsReport is already a + * JsonObject descendant, but we force it here so the user knows it's safe to serialize. + */ +export type AnalyticsForwarderFn = (report: JsonObject & AnalyticsReport) => void; + +/** + * A class that follows the Analytics interface and forwards analytic reports (JavaScript objects). + * AnalyticsReporter is the counterpart which takes analytic reports and report them to another + * Analytics interface. + */ +export class ForwardingAnalytics implements Analytics { + constructor(protected _fn: AnalyticsForwarderFn) {} + + event(category: string, action: string, options?: EventOptions) { + this._fn({ + kind: AnalyticsReportKind.Event, + category, + action, + options: { ...options } as JsonObject, + }); + } + screenview(screenName: string, appName: string, options?: ScreenviewOptions) { + this._fn({ + kind: AnalyticsReportKind.Screenview, + screenName, + appName, + options: { ...options } as JsonObject, + }); + } + pageview(path: string, options?: PageviewOptions) { + this._fn({ + kind: AnalyticsReportKind.Pageview, + path, + options: { ...options } as JsonObject, + }); + } + timing(category: string, variable: string, time: string | number, options?: TimingOptions): void { + this._fn({ + kind: AnalyticsReportKind.Timing, + category, + variable, + time, + options: { ...options } as JsonObject, + }); + } + + // We do not support flushing. + flush() { + return Promise.resolve(); + } +} + + +export class AnalyticsReporter { + constructor(protected _analytics: Analytics) {} + + report(report: AnalyticsReport) { + switch (report.kind) { + case AnalyticsReportKind.Event: + this._analytics.event(report.category, report.action, report.options); + break; + case AnalyticsReportKind.Screenview: + this._analytics.screenview(report.screenName, report.appName, report.options); + break; + case AnalyticsReportKind.Pageview: + this._analytics.pageview(report.path, report.options); + break; + case AnalyticsReportKind.Timing: + this._analytics.timing(report.category, report.variable, report.time, report.options); + break; + + default: + throw new Error('Unexpected analytics report: ' + JSON.stringify(report)); + } + } +} diff --git a/packages/angular_devkit/core/src/analytics/index.ts b/packages/angular_devkit/core/src/analytics/index.ts new file mode 100644 index 0000000000..e84c8b068b --- /dev/null +++ b/packages/angular_devkit/core/src/analytics/index.ts @@ -0,0 +1,12 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +export * from './api'; +export * from './forwarder'; +export * from './logging'; +export * from './multi'; +export * from './noop'; diff --git a/packages/angular_devkit/core/src/analytics/logging.ts b/packages/angular_devkit/core/src/analytics/logging.ts new file mode 100644 index 0000000000..e19785727f --- /dev/null +++ b/packages/angular_devkit/core/src/analytics/logging.ts @@ -0,0 +1,34 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { Logger } from '../logger'; +import { Analytics, EventOptions, PageviewOptions, ScreenviewOptions, TimingOptions } from './api'; + +/** + * Analytics implementation that logs analytics events to a logger. This should be used for + * debugging mainly. + */ +export class LoggingAnalytics implements Analytics { + constructor(protected _logger: Logger) {} + + event(category: string, action: string, options?: EventOptions): void { + this._logger.info('event ' + JSON.stringify({ category, action, ...options })); + } + screenview(screenName: string, appName: string, options?: ScreenviewOptions): void { + this._logger.info('screenview ' + JSON.stringify({ screenName, appName, ...options })); + } + pageview(path: string, options?: PageviewOptions): void { + this._logger.info('pageview ' + JSON.stringify({ path, ...options })); + } + timing(category: string, variable: string, time: string | number, options?: TimingOptions): void { + this._logger.info('timing ' + JSON.stringify({ category, variable, time, ...options })); + } + + flush(): Promise { + return Promise.resolve(); + } +} diff --git a/packages/angular_devkit/core/src/analytics/multi.ts b/packages/angular_devkit/core/src/analytics/multi.ts new file mode 100644 index 0000000000..141f0db38e --- /dev/null +++ b/packages/angular_devkit/core/src/analytics/multi.ts @@ -0,0 +1,38 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { Analytics, EventOptions, PageviewOptions, ScreenviewOptions, TimingOptions } from './api'; + +/** + * Analytics implementation that reports to multiple analytics backend. + */ +export class MultiAnalytics implements Analytics { + constructor(protected _backends: Analytics[] = []) {} + + push(...backend: Analytics[]) { + this._backends.push(...backend); + } + + event(category: string, action: string, options?: EventOptions): void { + this._backends.forEach(be => be.event(category, action, options)); + } + screenview(screenName: string, appName: string, options?: ScreenviewOptions): void { + this._backends.forEach(be => be.screenview(screenName, appName, options)); + } + pageview(path: string, options?: PageviewOptions): void { + this._backends.forEach(be => be.pageview(path, options)); + } + timing(category: string, variable: string, time: string | number, options?: TimingOptions): void { + this._backends.forEach(be => be.timing(category, variable, time, options)); + } + + + flush(): Promise { + return Promise.all(this._backends.map(x => x.flush())).then(() => {}); + } +} diff --git a/packages/angular_devkit/core/src/analytics/noop.ts b/packages/angular_devkit/core/src/analytics/noop.ts new file mode 100644 index 0000000000..a4592a75c1 --- /dev/null +++ b/packages/angular_devkit/core/src/analytics/noop.ts @@ -0,0 +1,19 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { Analytics } from './api'; + +/** + * Analytics implementation that does nothing. + */ +export class NoopAnalytics implements Analytics { + event() {} + screenview() {} + pageview() {} + timing() {} + flush(): Promise { return Promise.resolve(); } +} diff --git a/packages/angular_devkit/core/src/index.ts b/packages/angular_devkit/core/src/index.ts index e292e8e9ff..03272380bb 100644 --- a/packages/angular_devkit/core/src/index.ts +++ b/packages/angular_devkit/core/src/index.ts @@ -5,6 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ +import * as analytics from './analytics'; import * as experimental from './experimental'; import * as json from './json/index'; import * as logging from './logger/index'; @@ -16,6 +17,7 @@ export * from './utils/index'; export * from './virtual-fs/index'; export { + analytics, experimental, json, logging,