diff --git a/src/core/crosslytics.spec.ts b/src/core/crosslytics.spec.ts new file mode 100644 index 0000000..51c29b2 --- /dev/null +++ b/src/core/crosslytics.spec.ts @@ -0,0 +1,42 @@ +import test from "ava"; +import { Crosslytics } from "./crosslytics"; +import { TrackedEvent } from "./trackedEvent"; +import { Tracker } from "./tracker"; +import { Identity } from "./identity"; + +type TestEventArgs = { + "Color": string; +}; + +class TestEvent extends TrackedEvent { + name = "Test Event"; + category = "Test Category"; + organizationId = "abc123"; + argPriority = new Array(); +} + +class TestTracker implements Tracker { + public track(identity: Identity, event: TrackedEvent) { + return Promise.resolve(); + } +} + +test("Should only register a Tracker once", t => { + const cl = new Crosslytics(); + const tracker = new TestTracker(); + cl.registerTracker(tracker); + cl.registerTracker(tracker); + t.is(cl.trackers.length, 1); +}); + +test("Should deregister a Tracker", t => { + const cl = new Crosslytics(); + const trackerA = new TestTracker(); + const trackerB = new TestTracker(); + cl.registerTracker(trackerA); + cl.registerTracker(trackerB); + t.is(cl.trackers.length, 2); + cl.deregisterTracker(trackerB); + cl.deregisterTracker(trackerB); + t.is(cl.trackers.length, 1); +}); diff --git a/src/core/crosslytics.ts b/src/core/crosslytics.ts new file mode 100644 index 0000000..46224a0 --- /dev/null +++ b/src/core/crosslytics.ts @@ -0,0 +1,30 @@ +import { Identity } from "./identity"; +import { TrackedEvent } from "./trackedEvent"; +import { Tracker } from "./tracker"; + +/** + * Main API entry point + */ +export class Crosslytics { + public identity: Identity; + + public readonly trackers = new Array(); + public registerTracker(tracker: Tracker) { + if (~this.trackers.indexOf(tracker)) { + return; + } + + this.trackers.push(tracker); + } + + public deregisterTracker(tracker: Tracker) { + const index = this.trackers.indexOf(tracker); + if (~index) { + this.trackers.splice(index, 1); + } + } + + public async track(event: TrackedEvent) { + return this.trackers.map(t => t.track(this.identity, event)); + } +} diff --git a/src/core/identity.ts b/src/core/identity.ts new file mode 100644 index 0000000..269e904 --- /dev/null +++ b/src/core/identity.ts @@ -0,0 +1,16 @@ +import { Organization } from "./organization"; +import { Value } from "./value"; + +/** + * A user that you're tracking. Logically equivalent to an Identity in the Segment spec. + * @see {@link https://segment.com/docs/spec/identify/#identities} + */ +export interface Identity { + userId: string; + organization?: Organization; + traits?: { + email: string; + name: string; + [key: string]: Value; + }; +} diff --git a/src/core/organization.ts b/src/core/organization.ts new file mode 100644 index 0000000..f4866e7 --- /dev/null +++ b/src/core/organization.ts @@ -0,0 +1,13 @@ +import { Value } from "./value"; + +/** + * A group of users. Logically equivalent to a Group in the Segment spec. + * @see {@link https://segment.com/docs/spec/group/} + */ +export interface Organization { + organizationId: string; + traits?: { + name: string; + [key: string]: Value; + }; +} diff --git a/src/core/trackedEvent.ts b/src/core/trackedEvent.ts new file mode 100644 index 0000000..bcf9eec --- /dev/null +++ b/src/core/trackedEvent.ts @@ -0,0 +1,45 @@ +import { Value } from "./value"; + +/** + * A user action. Logically equivalent to an Event in the Segment spec. + * Pass in a type defining your event's arguments. + * @example + * type DashboardPanelEventArgs = { + * 'Panel ID': string, + * 'Panel Color'?: string, + * 'Panel Type'?: number, + * 'Panel Name'?: string + * }; + * class DashboardPanelCreated extends TrackedEvent { + * readonly name = 'DashboardPanel Created'; + * readonly category = 'Dashboard'; + * readonly argPriority: (keyof DashboardPanelEventArgs)[] = [ + * 'Panel ID', + * 'Panel Type', + * 'Panel Name', + * 'Panel Color' + * ]; + * } + * @see {@link https://segment.com/docs/spec/track/#event} + */ +export abstract class TrackedEvent { + /** + * We suggest human readable names consisting of noun + past tense verb. + * @see {@link https://segment.com/academy/collecting-data/naming-conventions-for-clean-data/} + */ + public abstract readonly name: string; + public abstract readonly category: string; + public organizationId: string; + + /** + * Many trackers only support a limited number of arguments. For example, + * Google Analytics only supports 2: a string Event Label and an integer + * Event Value. By defining a priority to your arguments here, trackers + * will submit the highest priority args satisfying their constraints. In + * the Google Analytics case, the tracker will submit the first string match + * as the Label and the first integer match as the Value. + */ + public abstract readonly argPriority: Array; + + constructor(public args: T) {} +} diff --git a/src/core/tracker.ts b/src/core/tracker.ts new file mode 100644 index 0000000..3c42ce0 --- /dev/null +++ b/src/core/tracker.ts @@ -0,0 +1,10 @@ +import { Identity } from "./identity"; +import { TrackedEvent } from "./trackedEvent"; + +/** + * A Tracker is a 3rd party analytics service such as Google Analytics or Intercom + * that ultimately receives your `TrackedEvent`s. + */ +export interface Tracker { + track(identity: Identity, event: TrackedEvent): Promise; +} diff --git a/src/core/value.ts b/src/core/value.ts new file mode 100644 index 0000000..5ba4a10 --- /dev/null +++ b/src/core/value.ts @@ -0,0 +1 @@ +export type Value = string | number; diff --git a/src/greeter.spec.ts b/src/greeter.spec.ts deleted file mode 100644 index af356aa..0000000 --- a/src/greeter.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import test from "ava"; -import { Greeter } from "./greeter"; - -test("Should greet with message", t => { - const greeter = new Greeter("friend"); - t.is(greeter.greet(), "Bonjour, friend!"); -}); diff --git a/src/greeter.ts b/src/greeter.ts deleted file mode 100644 index b384fef..0000000 --- a/src/greeter.ts +++ /dev/null @@ -1,11 +0,0 @@ -export class Greeter { - private greeting: string; - - constructor(message: string) { - this.greeting = message; - } - - public greet() { - return "Bonjour, " + this.greeting + "!"; - } -} diff --git a/src/index.spec.ts b/src/index.spec.ts deleted file mode 100644 index 5842274..0000000 --- a/src/index.spec.ts +++ /dev/null @@ -1,6 +0,0 @@ -import test from "ava"; -import * as index from "./index"; - -test("Should have Greeter available", t => { - t.truthy(index.Greeter); -}); diff --git a/src/index.ts b/src/index.ts index a99cfb6..a2d1b1c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,5 @@ -export * from "./greeter"; +export * from "./core/crosslytics"; +export * from "./core/identity"; +export * from "./core/organization"; +export * from "./core/trackedEvent"; +export * from "./core/tracker"; diff --git a/tsconfig.json b/tsconfig.json index 2ea4548..9a9bc07 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,8 +6,9 @@ "lib": [ "esnext" ], - "target": "es2015", + "target": "es6", "noImplicitAny": true, + "strictNullChecks": true, "outDir": "./lib", "preserveConstEnums": true, "removeComments": true, diff --git a/tsconfig.test.json b/tsconfig.test.json index e7bd7c7..5988f49 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es5", + "target": "es6", "outDir": "lib_test", "declaration": false, "noImplicitAny": true, diff --git a/tslint.json b/tslint.json index 09b2fba..459b7a5 100644 --- a/tslint.json +++ b/tslint.json @@ -2,5 +2,9 @@ "extends": [ "tslint:latest", "tslint-config-prettier" - ] + ], + "rules": { + "interface-name": [true, "never-prefix"], + "quotemark": [ true, "single" ] + } }