Skip to content

Commit

Permalink
Merge pull request #50 from manifoldco/sslotsky/analytics
Browse files Browse the repository at this point in the history
Analytics tracking in manifold-init
  • Loading branch information
sslotsky authored Mar 19, 2020
2 parents 0c83e38 + c622c67 commit e1d517f
Show file tree
Hide file tree
Showing 8 changed files with 317 additions and 19 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,5 @@ www/
$RECYCLE.BIN/

.npmrc

manifold-init-types/types/
5 changes: 5 additions & 0 deletions manifold-init-types/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion manifold-init-types/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@manifoldco/manifold-init-types",
"version": "0.0.1-next.26",
"version": "0.4.0-beta.0",
"private": false,
"description": "TypeScript types for manifold-init",
"author": "manifoldco",
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"loader/",
"src/"
],
"version": "0.0.1-next.26",
"version": "0.4.0-beta.0",
"description": "Manifold UI Initialization",
"main": "dist/index.js",
"module": "dist/index.mjs",
Expand Down
135 changes: 135 additions & 0 deletions src/v0/__tests__/analytics.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import fetchMock from 'fetch-mock';
import createAnalytics, { AnalyticsEvent, ErrorEvent } from '../analytics';

const local = 'begin:http://analytics.arigato.tools';
const stage = 'begin:https://analytics.stage.manifold.co';
const prod = 'begin:https://analytics.manifold.co';

const metric: AnalyticsEvent = {
type: 'metric',
name: 'rtt_graphql',
properties: {
duration: 123,
},
};
const error: ErrorEvent = {
type: 'error',
name: 'mui-pricing-matrix_error',
properties: {
code: 'code',
version: '1.2.3',
message: 'message',
clientId: '123',
},
};
const track: AnalyticsEvent = {
type: 'component-analytics',
name: 'click',
properties: {
planId: '1234',
},
};

describe('analytics', () => {
describe('env', () => {
afterEach(fetchMock.restore);

it('local', async () => {
const analytics = createAnalytics({
env: 'local',
element: document.createElement('manifold-product'),
componentVersion: '1.2.3',
});
fetchMock.mock(local, {});
await analytics.track(error);
expect(fetchMock.called(local)).toBe(true);
});

it('stage', async () => {
const analytics = createAnalytics({
env: 'stage',
element: document.createElement('manifold-product'),
componentVersion: '1.2.3',
});
fetchMock.mock(stage, {});
await analytics.track(error);
expect(fetchMock.called(stage)).toBe(true);
});

it('prod', async () => {
const analytics = createAnalytics({
env: 'prod',
element: document.createElement('manifold-product'),
componentVersion: '1.2.3',
});
fetchMock.mock(prod, {});
await analytics.track(error);
expect(fetchMock.called(prod)).toBe(true);
});
});

describe('type', () => {
beforeEach(() => fetchMock.mock(prod, {}));
afterEach(fetchMock.restore);

it('error', async () => {
const analytics = createAnalytics({
env: 'prod',
element: document.createElement('manifold-product'),
componentVersion: '1.2.3',
});
await analytics.track(error);
const res = fetchMock.calls()[0][1];
expect(res && res.body && JSON.parse(res.body.toString())).toEqual({
...error,
source: 'MANIFOLD-PRODUCT',
properties: {
...error.properties,
componentName: 'MANIFOLD-PRODUCT',
},
});
});

it('metric', async () => {
const componentVersion = '1.2.3';
const analytics = createAnalytics({
env: 'prod',
element: document.createElement('manifold-product'),
componentVersion,
});
await analytics.track(metric);
const res = fetchMock.calls()[0][1];
expect(res && res.body && JSON.parse(res.body.toString())).toEqual({
...metric,
source: 'MANIFOLD-PRODUCT',
properties: {
...metric.properties,
componentName: 'MANIFOLD-PRODUCT',
version: componentVersion,
duration: '123', // numbers should submit as strings
},
});
});

it('track', async () => {
const componentVersion = '1.2.3';
const analytics = createAnalytics({
env: 'prod',
element: document.createElement('manifold-product'),
componentVersion,
});
await analytics.track(track);
const res = fetchMock.calls()[0][1];
expect(res && res.body && JSON.parse(res.body.toString())).toEqual({
...track,
source: 'MANIFOLD-PRODUCT',
properties: {
...track.properties,
componentName: 'MANIFOLD-PRODUCT',
version: componentVersion,
planId: '1234', // numbers should submit as strings
},
});
});
});
});
144 changes: 144 additions & 0 deletions src/v0/analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/**
* Properties that should be found in every analytics event
*/
interface SharedProperties {
description?: string;
}

/**
* Based on `name`, what data should be sent?
*/
export type EventTypes =
| {
name: 'load';
properties: {
duration: number;
};
}
| {
name: 'first_render';
properties: {
duration: number;
};
}
| {
name: 'rtt_graphql';
properties: {
duration: number;
};
}
| {
name: 'token_received';
properties: {
duration: number;
};
}
| {
name: 'first_render_with_data';
properties: {
duration: number;
rttGraphql: number;
load: number;
};
}
| {
name: 'click';
properties: {
planId: string;
};
};

export type EventEvent = {
type: 'metric' | 'component-analytics';
} & SharedProperties &
EventTypes;

/**
* Error analytics event
*/
export interface ErrorEvent extends SharedProperties {
type: 'error';
name: string;
properties: {
code: string;
message: string;
version: string;
clientId?: string;
};
}

export interface ErrorDetail {
name: string;
code?: string;
message?: string;
}

export type AnalyticsEvent = ErrorEvent | EventEvent;

export const endpoint = {
local: 'http://analytics.arigato.tools/v1/events',
stage: 'https://analytics.stage.manifold.co/v1/events',
prod: 'https://analytics.manifold.co/v1/events',
};

export interface CreateAnalytics {
env: 'prod' | 'stage' | 'local';
element: HTMLElement;
componentVersion: string;
clientId?: string;
}

export default function createAnalytics(args: CreateAnalytics) {
function stringifyProperties(evt: AnalyticsEvent) {
return {
...evt,
source: args.element.tagName,
properties: Object.entries(evt.properties).reduce(
(properties, [key, value]) => ({ ...properties, [key]: `${value}` }),
{
componentName: args.element.tagName,
version: args.componentVersion,
clientId: args.clientId,
}
),
};
}

/**
* Report an error or analytics event to Manifold
* @param {Object} eventData Event data to send to Manifold
* @param {string} eventData.type 'event' or 'error'
* @param {string} eventData.name name_of_event (lowercase with underscores)
* @param {string} [eventData.description] User-readable description of this event
* @param {Object} eventData.properties Free-form object of event properties (different names will require different properties)
* @param {Object} [options] Analytics options
* @param {string} [options.env] 'prod' (default) or 'stage'
*/
function track(evt: AnalyticsEvent) {
const url = endpoint[args.env];

return fetch(url, {
method: 'POST',
body: JSON.stringify(stringifyProperties(evt)),
});
}

function report(detail: ErrorDetail) {
track({
type: 'error',
name: detail.name,
properties: {
code: detail.code || '',
message: detail.message || '',
version: args.componentVersion,
clientId: args.clientId || '',
},
});

console.error(detail); // report error (Rollbar, Datadog, etc.)
const evt = new CustomEvent('manifold-error', { bubbles: true, detail });
args.element.dispatchEvent(evt);
}

return { track, report };
}
42 changes: 27 additions & 15 deletions src/v0/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
import createAnalytics, { AnalyticsEvent, ErrorDetail } from './analytics';
import { createGraphqlFetch, GraphqlFetch } from './graphqlFetch';
import { createGateway, Gateway } from './gateway';

// TODO add real tracking
const track = data => {
// eslint-disable-next-line no-console
console.log(data);
};

export interface Connection {
graphqlFetch: GraphqlFetch;
gateway: Gateway;
track: (event: string) => void;
analytics: {
track: (e: AnalyticsEvent) => Promise<Response>;
report: (detail: ErrorDetail) => void;
};
}

const connection = (options: {
env: 'stage' | 'prod';
env: 'stage' | 'prod' | 'local';
element: HTMLElement;
componentVersion: string;
clientId?: string;
Expand All @@ -23,19 +21,33 @@ const connection = (options: {

return {
gateway: createGateway({
baseUrl: () =>
env === 'stage' ? 'https://api.stage.manifold.co/v1' : 'https://api.manifold.co/v1',
baseUrl: () => {
switch (env) {
case 'stage':
return 'https://api.stage.manifold.co/v1';
case 'local':
return 'https://api.arigato.tools/v1';
default:
return 'https://api.manifold.co/v1';
}
},
}),
graphqlFetch: createGraphqlFetch({
element,
version: componentVersion,
clientId,
endpoint: () =>
env === 'stage'
? 'https://api.stage.manifold.co/graphql'
: 'https://api.manifold.co/graphql',
endpoint: () => {
switch (env) {
case 'stage':
return 'https://api.stage.manifold.co/graphql';
case 'local':
return 'https://api.arigato.tools/graphql';
default:
return 'https://api.manifold.co/graphql';
}
},
}),
track,
analytics: createAnalytics({ env, element, componentVersion, clientId }),
};
};

Expand Down

0 comments on commit e1d517f

Please sign in to comment.