Skip to content

Commit

Permalink
feat: custom error handlers (#52)
Browse files Browse the repository at this point in the history
From integrating Typewriter at Segment, we've learned that throwing an error whenever Typewriter discovers a validation issues is a pretty poor DX because it forces you to align your instrumentation and spec from day 1. Instead, we want to offer developers the opportunity to iteratively adopt Typewriter.

To do so, this PR exposes an `onError` handler that can be configured at run-time when constructing an instance of a Typewriter client.

As an example, this is what happens now when you have a validation error:

![image](https://user-images.githubusercontent.com/2907397/52014309-0f2edd00-2494-11e9-872a-d9e0b86e521f.png)

Your entire app breaks while developing locally -- and that interrupts your developer flow.

Now, you can hook in to that error handling logic like so:

```js
const analytics = new Analytics(window.analytics, {
   onError: (error) => {
       console.error(JSON.stringify(error, null, 2))
   }
})
```

After doing this, the errors will get logged instead:

![image](https://user-images.githubusercontent.com/2907397/52014714-228e7800-2495-11e9-9e68-84d3cdf7cee6.png)

This allows you to handle errors however you want! For example, you could `alert` on errors:

![image](https://user-images.githubusercontent.com/2907397/52014438-5f0da400-2494-11e9-8744-263d82ef5f48.png)

Or you could use this to ship validation to production without crashing your app and forward validation issues to Sentry/BugSnag/etc.
  • Loading branch information
colinking authored Jan 31, 2019
1 parent 54d484e commit 7f073d8
Show file tree
Hide file tree
Showing 19 changed files with 488 additions and 75 deletions.
26 changes: 25 additions & 1 deletion examples/gen-js/js/analytics/generated/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,32 @@ export interface ProfileViewed {
* Analytics provides a strongly-typed wrapper around Segment Analytics
* based on your Tracking Plan.
*/

// From https://github.com/epoberezkin/ajv/blob/0c31c1e2a81e315511c60a0dd7420a72cb181e61/lib/ajv.d.ts#L279
interface AjvErrorObject {
keyword: string;
dataPath: string;
schemaPath: string;
params: object;
message: string;
propertyName?: string;
parentSchema?: object;
data?: any;
}

// An invalid event with its associated collection of validation errors.
interface InvalidEvent {
eventName: string;
validationErrors: AjvErrorObject[];
}

// Options to customize the runtime behavior of a Typewriter client.
interface AnalyticsOptions {
onError(event: InvalidEvent): void;
}

export default class Analytics {
constructor(analytics: any);
constructor(analytics: any, options?: AnalyticsOptions);

feedViewed(
props?: FeedViewed,
Expand Down
30 changes: 25 additions & 5 deletions examples/gen-js/js/analytics/generated/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
export default class Analytics {
/**
* Instantiate a wrapper around an analytics library instance
* @param {Analytics} analytics - The analytics.js library to wrap
* @param {Analytics} analytics The analytics.js library to wrap
* @param {Object} [options] Optional configuration of the Typewriter client
* @param {function} [options.onError] Error handler fired when run-time validation errors
* are raised.
*/
constructor(analytics) {
constructor(analytics, options = {}) {
if (!analytics) {
throw new Error("An instance of analytics.js must be provided");
}
this.analytics = analytics || { track: () => null };
this.onError =
options.onError ||
(error => {
throw new Error(JSON.stringify(error, null, 2));
});
}
addTypewriterContext(context = {}) {
return {
Expand Down Expand Up @@ -97,7 +105,11 @@ export default class Analytics {
return errors === 0;
};
if (!validate({ properties: props })) {
throw new Error(JSON.stringify(validate.errors, null, 2));
this.onError({
eventName: "Feed Viewed",
validationErrors: validate.errors
});
return;
}
this.analytics.track(
"Feed Viewed",
Expand Down Expand Up @@ -188,7 +200,11 @@ export default class Analytics {
return errors === 0;
};
if (!validate({ properties: props })) {
throw new Error(JSON.stringify(validate.errors, null, 2));
this.onError({
eventName: "Photo Viewed",
validationErrors: validate.errors
});
return;
}
this.analytics.track(
"Photo Viewed",
Expand Down Expand Up @@ -279,7 +295,11 @@ export default class Analytics {
return errors === 0;
};
if (!validate({ properties: props })) {
throw new Error(JSON.stringify(validate.errors, null, 2));
this.onError({
eventName: "Profile Viewed",
validationErrors: validate.errors
});
return;
}
this.analytics.track(
"Profile Viewed",
Expand Down
26 changes: 25 additions & 1 deletion examples/gen-js/node/analytics/generated/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,32 @@ export interface Product {
* Analytics provides a strongly-typed wrapper around Segment Analytics
* based on your Tracking Plan.
*/

// From https://github.com/epoberezkin/ajv/blob/0c31c1e2a81e315511c60a0dd7420a72cb181e61/lib/ajv.d.ts#L279
interface AjvErrorObject {
keyword: string;
dataPath: string;
schemaPath: string;
params: object;
message: string;
propertyName?: string;
parentSchema?: object;
data?: any;
}

// An invalid event with its associated collection of validation errors.
interface InvalidEvent {
eventName: string;
validationErrors: AjvErrorObject[];
}

// Options to customize the runtime behavior of a Typewriter client.
interface AnalyticsOptions {
onError(event: InvalidEvent): void;
}

export default class Analytics {
constructor(analytics: any);
constructor(analytics: any, options?: AnalyticsOptions);

orderCompleted(
message?: TrackMessage<OrderCompleted>,
Expand Down
18 changes: 15 additions & 3 deletions examples/gen-js/node/analytics/generated/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
class Analytics {
/**
* Instantiate a wrapper around an analytics library instance
* @param {Analytics} analytics - The analytics-node library to wrap
* @param {Analytics} analytics The analytics-node library to wrap
* @param {Object} [options] Optional configuration of the Typewriter client
* @param {function} [options.onError] Error handler fired when run-time validation errors
* are raised.
*/
constructor(analytics) {
constructor(analytics, options = {}) {
if (!analytics) {
throw new Error("An instance of analytics-node must be provided");
}
this.analytics = analytics || { track: () => null };
this.onError =
options.onError ||
(error => {
throw new Error(JSON.stringify(error, null, 2));
});
}
addTypewriterContext(context = {}) {
return Object.assign({}, context, {
Expand Down Expand Up @@ -563,7 +571,11 @@ class Analytics {
return errors === 0;
};
if (!validate(message)) {
throw new Error(JSON.stringify(validate.errors, null, 2));
this.onError({
eventName: "Order Completed",
validationErrors: validate.errors
});
return;
}
message = Object.assign({}, message, {
context: this.addTypewriterContext(message.context),
Expand Down
26 changes: 25 additions & 1 deletion examples/gen-js/ts/analytics/generated/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,32 @@ export interface ProfileViewed {
* Analytics provides a strongly-typed wrapper around Segment Analytics
* based on your Tracking Plan.
*/

// From https://github.com/epoberezkin/ajv/blob/0c31c1e2a81e315511c60a0dd7420a72cb181e61/lib/ajv.d.ts#L279
interface AjvErrorObject {
keyword: string;
dataPath: string;
schemaPath: string;
params: object;
message: string;
propertyName?: string;
parentSchema?: object;
data?: any;
}

// An invalid event with its associated collection of validation errors.
interface InvalidEvent {
eventName: string;
validationErrors: AjvErrorObject[];
}

// Options to customize the runtime behavior of a Typewriter client.
interface AnalyticsOptions {
onError(event: InvalidEvent): void;
}

export default class Analytics {
constructor(analytics: any);
constructor(analytics: any, options?: AnalyticsOptions);

feedViewed(
props?: FeedViewed,
Expand Down
30 changes: 25 additions & 5 deletions examples/gen-js/ts/analytics/generated/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
export default class Analytics {
/**
* Instantiate a wrapper around an analytics library instance
* @param {Analytics} analytics - The analytics.js library to wrap
* @param {Analytics} analytics The analytics.js library to wrap
* @param {Object} [options] Optional configuration of the Typewriter client
* @param {function} [options.onError] Error handler fired when run-time validation errors
* are raised.
*/
constructor(analytics) {
constructor(analytics, options = {}) {
if (!analytics) {
throw new Error("An instance of analytics.js must be provided");
}
this.analytics = analytics || { track: () => null };
this.onError =
options.onError ||
(error => {
throw new Error(JSON.stringify(error, null, 2));
});
}
addTypewriterContext(context = {}) {
return {
Expand Down Expand Up @@ -97,7 +105,11 @@ export default class Analytics {
return errors === 0;
};
if (!validate({ properties: props })) {
throw new Error(JSON.stringify(validate.errors, null, 2));
this.onError({
eventName: "Feed Viewed",
validationErrors: validate.errors
});
return;
}
this.analytics.track(
"Feed Viewed",
Expand Down Expand Up @@ -188,7 +200,11 @@ export default class Analytics {
return errors === 0;
};
if (!validate({ properties: props })) {
throw new Error(JSON.stringify(validate.errors, null, 2));
this.onError({
eventName: "Photo Viewed",
validationErrors: validate.errors
});
return;
}
this.analytics.track(
"Photo Viewed",
Expand Down Expand Up @@ -279,7 +295,11 @@ export default class Analytics {
return errors === 0;
};
if (!validate({ properties: props })) {
throw new Error(JSON.stringify(validate.errors, null, 2));
this.onError({
eventName: "Profile Viewed",
validationErrors: validate.errors
});
return;
}
this.analytics.track(
"Profile Viewed",
Expand Down
15 changes: 12 additions & 3 deletions src/commands/gen-js/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,23 @@ export function genJS(
if (!analytics) {
throw new Error('An instance of ${clientName} must be provided')
}`
const errorHandler = `
this.onError = options.onError || (error => {
throw new Error(JSON.stringify(error, null, 2));
})`
const fileHeader = `
export default class Analytics {
/**
* Instantiate a wrapper around an analytics library instance
* @param {Analytics} analytics - The ${clientName} library to wrap
* @param {Analytics} analytics The ${clientName} library to wrap
* @param {Object} [options] Optional configuration of the Typewriter client
* @param {function} [options.onError] Error handler fired when run-time validation errors
* are raised.
*/
constructor(analytics) {
constructor(analytics, options = {}) {
${runtimeValidation ? analyticsValidation : ''}
this.analytics = analytics || { track: () => null }
${runtimeValidation ? errorHandler : ''}
}
addTypewriterContext(context = {}) {
Expand Down Expand Up @@ -82,7 +90,8 @@ export function genJS(
const validationCode = `
${compiledValidationFn}
if (!${validateCall}) {
throw new Error(JSON.stringify(validate.errors, null, 2));
this.onError({ eventName: '${name}', validationErrors: validate.errors })
return
}`

return `
Expand Down
26 changes: 25 additions & 1 deletion src/commands/gen-js/typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,8 +283,32 @@ class AJSTSDeclarationsRenderer extends TypeScriptRenderer {
'Analytics provides a strongly-typed wrapper around Segment Analytics',
'based on your Tracking Plan.'
])
this.emitLine(`
// From https://github.com/epoberezkin/ajv/blob/0c31c1e2a81e315511c60a0dd7420a72cb181e61/lib/ajv.d.ts#L279
interface AjvErrorObject {
keyword: string;
dataPath: string;
schemaPath: string;
params: object;
message: string;
propertyName?: string;
parentSchema?: object;
data?: any;
}
// An invalid event with its associated collection of validation errors.
interface InvalidEvent {
eventName: string;
validationErrors: AjvErrorObject[];
}
// Options to customize the runtime behavior of a Typewriter client.
interface AnalyticsOptions {
onError(event: InvalidEvent): void;
}
`)
this.emitBlock('export default class Analytics', '', () => {
this.emitLine('constructor(analytics: any)')
this.emitLine('constructor(analytics: any, options?: AnalyticsOptions)')
this.ensureBlankLine()

this.emitAnalyticsFunctions()
Expand Down
Loading

0 comments on commit 7f073d8

Please sign in to comment.