diff --git a/README.md b/README.md index 8be77b0..4da624a 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,55 @@ Note that the Express middleware handler will pick up and transmit any `err` obj ## Documentation -### Callbacks +### Send + +The `send()` function is asynchronous and returns a `Promise` of type `IncomingMessage`. + +Note that `IncomingMessage` can be `null` if the request was stored because the application was offline. + +`IncomingMessage` is the response from the Raygun API - there's nothing in the body, it's just a status code response. +If everything went ok, you'll get a 202 response code. +Otherwise, we throw 401 for incorrect API keys, 403 if you're over your plan limits, or anything in the 500+ range for internal errors. + +We use the nodejs http/https library to make the POST to Raygun, you can see more documentation about that callback here: https://nodejs.org/api/http.html#http_http_request_options_callback + +You can `await` the call to obtain the result, or use `then/catch`. + +#### Using `await` + +Use `await` to obtain the `IncomingMessage`, remember to `catch` any possible thrown errors from the `send()` method. + +```js +try { + let message = await client.send(error); +} catch (e) { + // error sending message +} +``` + +#### Using `then/catch` + +You can also use `then()` to obtain the `IncomingMessage`, as well, use `catch()` to catch any possible thrown errors from the `send()` method. + +```js +client.send(error) + .then((message) => { + // message sent to Raygun + }) + .catch((error) => { + // error sending message + }); +``` + +### Legacy `sendWithCallback` + +```javascript +client.sendWithCallback(new Error(), {}, function (response){ }); +``` + +The client still provides a legacy `send()` method that supports callbacks instead of `Promises`. + +**This method is deprecated and will be removed in the future.** The callback should be a node-style callback: `function(err, response) { /*...*/ }`. *Note*: If the callback only takes one parameter (`function(response){ /*...*/ }`) @@ -98,7 +146,7 @@ backwards compatibility; the Node-style callback should be preferred. You can pass custom data in on the Send() function, as the second parameter. For instance (based off the call in test/raygun_test.js): ```javascript -client.send(new Error(), { 'mykey': 'beta' }, function (response){ }); +client.send(new Error(), { 'mykey': 'beta' }); ``` #### Sending custom data with Expressjs @@ -113,19 +161,11 @@ raygunClient.expressCustomData = function (err, req) { }; ``` -### Callback - -```javascript -client.send(new Error(), {}, function (response){ }); -``` - -The argument to the 3rd argument callback is the response from the Raygun API - there's nothing in the body, it's just a status code response. If everything went ok, you'll get a 202 response code. Otherwise we throw 401 for incorrect API keys, 403 if you're over your plan limits, or anything in the 500+ range for internal errors. We use the nodejs http/https library to make the POST to Raygun, you can see more documentation about that callback here: https://nodejs.org/api/http.html#http_http_request_options_callback - ### Sending request data You can send the request data in the Send() function, as the fourth parameter. For example: ```javascript -client.send(new Error(), {}, function () {}, request); +client.send(new Error(), {}, request); ``` If you want to filter any of the request data then you can pass in an array of keys to filter when @@ -139,7 +179,7 @@ const raygunClient = new raygun.Client().init({ apiKey: 'YOUR_API_KEY', filters: You can add tags to your error in the Send() function, as the fifth parameter. For example: ```javascript -client.send(new Error(), {}, function () {}, {}, ['Custom Tag 1', 'Important Error']); +client.send(new Error(), {}, {}, ['Custom Tag 1', 'Important Error']); ``` Tags can also be set globally using setTags diff --git a/examples/using-domains/app.js b/examples/using-domains/app.js index 1542996..e644e4b 100644 --- a/examples/using-domains/app.js +++ b/examples/using-domains/app.js @@ -15,20 +15,21 @@ var appDomain = require("domain").create(); // Add the error handler so we can pass errors to Raygun when the domain // crashes appDomain.on("error", function (err) { - try { - console.log(`Domain error caught: ${err}`); - // Try send data to Raygun - raygunClient.send(err, {}, function () { + console.log(`Domain error caught: ${err}`); + // Try send data to Raygun + raygunClient + .send(err) + .then((message) => { // Exit the process once the error has been sent console.log("Error sent to Raygun, exiting process"); process.exit(1); + }) + .catch((error) => { + // If there was an error sending to Raygun, log it out and end the process. + // Could possibly log out to a text file here + console.log(error); + process.exit(1); }); - } catch (e) { - // If there was an error sending to Raygun, log it out and end the process. - // Could possibly log out to a text file here - console.log(e); - process.exit(1); - } }); // Run the domain diff --git a/lib/raygun.batch.ts b/lib/raygun.batch.ts index c71ef77..4160389 100644 --- a/lib/raygun.batch.ts +++ b/lib/raygun.batch.ts @@ -43,6 +43,11 @@ export class RaygunBatchTransport { this.httpOptions = options.httpOptions; } + /** + * Enqueues send request to batch processor. + * Callback in SendOptions is called when the message is eventually processed. + * @param options + */ send(options: SendOptions) { this.onIncomingMessage({ serializedMessage: options.message, @@ -145,6 +150,7 @@ export class RaygunBatchTransport { ); } + // TODO: Callbacks are processed in batch, see how can this be implemented with Promises for (const callback of callbacks) { if (callback) { callVariadicCallback(callback, err, response); diff --git a/lib/raygun.offline.ts b/lib/raygun.offline.ts index 2782a8e..3c13629 100644 --- a/lib/raygun.offline.ts +++ b/lib/raygun.offline.ts @@ -32,6 +32,7 @@ export class OfflineStorage implements IOfflineStorage { path.join(this.cachePath, item), "utf8", (err, cacheContents) => { + // TODO: MessageTransport ignores any errors from the send callback, could this be improved? this.transport.send(cacheContents); fs.unlink(path.join(this.cachePath, item), () => {}); }, diff --git a/lib/raygun.sync.transport.ts b/lib/raygun.sync.transport.ts index 15aac77..e8d9317 100644 --- a/lib/raygun.sync.transport.ts +++ b/lib/raygun.sync.transport.ts @@ -21,10 +21,17 @@ function syncRequest(httpOptions: SendOptionsWithoutCB) { console.log(requestProcess.stdout.toString()); } +/** + * Spawns a synchronous send request. + * Errors are not returned and callback is ignored. + * Only used to report uncaught exceptions. + * @param options + */ export function send(options: SendOptionsWithoutCB) { try { syncRequest(options); } catch (e) { + // TODO: Is there a reason we ignore errors here? console.log( `Raygun: error ${e} occurred while attempting to send error with message: ${options.message}`, ); diff --git a/lib/raygun.transport.ts b/lib/raygun.transport.ts index aa78f49..170b6d6 100644 --- a/lib/raygun.transport.ts +++ b/lib/raygun.transport.ts @@ -26,6 +26,12 @@ export function sendBatch(options: SendOptions) { return send(options, BATCH_ENDPOINT); } +// TODO: Convert this method callbacks to Promise. +/** + * Transport implementation that sends error to Raygun. + * Errors are reported back via callback. + * @param options + */ export function send(options: SendOptions, path = DEFAULT_ENDPOINT) { try { const data = Buffer.from(options.message); @@ -66,6 +72,7 @@ export function send(options: SendOptions, path = DEFAULT_ENDPOINT) { request.write(data); request.end(); } catch (e) { + // TODO: Non-HTTP errors are being ignored, should be better pass them up? console.log( `Raygun: error ${e} occurred while attempting to send error with message: ${options.message}`, ); diff --git a/lib/raygun.ts b/lib/raygun.ts index d7552a0..e7ef3e1 100644 --- a/lib/raygun.ts +++ b/lib/raygun.ts @@ -120,6 +120,7 @@ class Raygun { } this.expressHandler = this.expressHandler.bind(this); + this.sendWithCallback = this.sendWithCallback.bind(this); this.send = this.send.bind(this); this._offlineStorage = @@ -185,10 +186,45 @@ class Raygun { return raygunTransport; } - send( + /** + * Sends exception to Raygun. + * @param exception to send. + * @param customData to attach to the error report. + * @param request custom RequestParams. + * @param tags to attach to the error report. + * @return IncomingMessage if message was delivered, null if stored, rejected with Error if failed. + */ + async send( exception: Error | string, customData?: CustomData, - callback?: (err: Error | null) => void, + request?: RequestParams, + tags?: Tag[], + ): Promise { + // Convert internal sendWithCallback implementation to a Promise. + return new Promise((resolve, reject) => { + this.sendWithCallback( + exception, + customData, + function (err, message) { + if (err != null) { + reject(err); + } else { + resolve(message); + } + }, + request, + tags, + ); + }); + } + + /** + * @deprecated sendWithCallback is a deprecated method. Instead, use send, which supports async/await calls. + */ + sendWithCallback( + exception: Error | string, + customData?: CustomData, + callback?: Callback, request?: RequestParams, tags?: Tag[], ): Message { @@ -212,11 +248,14 @@ class Raygun { const sendOptions = sendOptionsResult.options; if (this._isOffline) { - this.offlineStorage().save( - JSON.stringify(message), - callback || emptyCallback, - ); + // make the save callback type compatible with Callback + const saveCallback = callback + ? (err: Error | null) => callVariadicCallback(callback, err, null) + : emptyCallback; + this.offlineStorage().save(JSON.stringify(message), saveCallback); } else { + // Use current transport to send request. + // Transport can be batch or default. this.transport().send(sendOptions); } @@ -245,20 +284,14 @@ class Raygun { }); } - private sendSync( - exception: Error | string, - customData?: CustomData, - callback?: (err: Error | null) => void, - request?: RequestParams, - tags?: Tag[], - ): void { - const result = this.buildSendOptions( - exception, - customData, - callback, - request, - tags, - ); + /** + * Send error using synchronous transport. + * Only used to report uncaught exceptions. + * @param exception error to report + * @private + */ + private sendSync(exception: Error | string): void { + const result = this.buildSendOptions(exception); if (result.valid) { raygunSyncTransport.send(result.options); @@ -285,9 +318,11 @@ class Raygun { body: req.body, }; - this.send(err, customData || {}, function () {}, requestParams, [ + this.send(err, customData || {}, requestParams, [ "UnhandledException", - ]); + ]).catch((err) => { + console.log(`[Raygun] Failed to send Express error: ${err}`); + }); next(err); } @@ -403,6 +438,7 @@ class Raygun { }; return { + // TODO: MessageTransport ignores any errors from the send callback, could this be improved? send(message: string) { transport.send({ message, diff --git a/lib/types.ts b/lib/types.ts index a869c6c..3c9ebda 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -68,6 +68,7 @@ export type Tag = string; export type SendOptions = { message: string; + // TODO: Remove Callback in SendOptions and use Promises internally callback: Callback; http: HTTPOptions; }; diff --git a/test/raygun_async_send_test.js b/test/raygun_async_send_test.js new file mode 100644 index 0000000..3d270c2 --- /dev/null +++ b/test/raygun_async_send_test.js @@ -0,0 +1,160 @@ +"use strict"; + +const test = require("tap").test; +const VError = require("verror"); +const nock = require("nock"); +const Raygun = require("../lib/raygun.ts"); + +nock(/.*/) + .post(/.*/, function () { + return true; + }) + .reply(202, {}) + .persist(); +const API_KEY = "apikey"; + +test("async send basic", {}, function (t) { + t.plan(1); + + let client = new Raygun.Client().init({ + apiKey: API_KEY, + }); + client + .send(new Error()) + .then((response) => { + t.equal(response.statusCode, 202); + t.end(); + }) + .catch((err) => { + t.fail(err); + }); +}); + +test("async send complex", {}, function (t) { + t.plan(1); + + let client = new Raygun.Client() + .init({ apiKey: API_KEY }) + .setUser("callum@mindscape.co.nz") + .setVersion("1.0.0.0"); + + client + .send(new Error()) + .then((response) => { + t.equal(response.statusCode, 202); + t.end(); + }) + .catch((err) => { + t.fail(err); + }); +}); + +test("async send with inner error", {}, function (t) { + t.plan(1); + + let error = new Error("Outer"); + let innerError = new Error("Inner"); + + error.cause = function () { + return innerError; + }; + + let client = new Raygun.Client().init({ + apiKey: API_KEY, + }); + client + .send(new Error()) + .then((response) => { + t.equal(response.statusCode, 202); + t.end(); + }) + .catch((err) => { + t.fail(err); + }); +}); + +test("async send with verror", {}, function (t) { + t.plan(1); + + let error = new VError( + new VError(new VError("Deep Error"), "Inner Error"), + "Outer Error", + ); + + let client = new Raygun.Client().init({ + apiKey: API_KEY, + }); + client + .send(error) + .then((response) => { + t.equal(response.statusCode, 202); + t.end(); + }) + .catch((err) => { + t.fail(err); + }); +}); + +test("async send with OnBeforeSend", {}, function (t) { + t.plan(1); + + let client = new Raygun.Client().init({ + apiKey: API_KEY, + }); + + let onBeforeSendCalled = false; + client.onBeforeSend(function (payload) { + onBeforeSendCalled = true; + return payload; + }); + + client + .send(new Error()) + .then((response) => { + t.equal(onBeforeSendCalled, true); + t.end(); + }) + .catch((err) => { + t.fail(err); + }); +}); + +test("check that tags get passed through in async send", {}, function (t) { + let tag = ["Test"]; + let client = new Raygun.Client().init({ apiKey: "TEST" }); + + client.setTags(tag); + + client.onBeforeSend(function (payload) { + t.same(payload.details.tags, tag); + return payload; + }); + + client + .send(new Error()) + .then((message) => { + t.end(); + }) + .catch((err) => { + t.fail(err); + }); +}); + +test("check that tags get merged", {}, function (t) { + let client = new Raygun.Client().init({ apiKey: "TEST" }); + client.setTags(["Tag1"]); + + client.onBeforeSend(function (payload) { + t.same(payload.details.tags, ["Tag1", "Tag2"]); + return payload; + }); + + client + .send(new Error(), {}, null, ["Tag2"]) + .then((message) => { + t.end(); + }) + .catch((err) => { + t.fail(err); + }); +}); diff --git a/test/raygun_offline_test.js b/test/raygun_offline_test.js index aff1cd0..69e058d 100644 --- a/test/raygun_offline_test.js +++ b/test/raygun_offline_test.js @@ -17,18 +17,7 @@ test("offline message storage and sending", async function (t) { }, }); const raygunClient = testEnvironment.client; - const send = (e) => - new Promise((resolve, reject) => { - raygunClient.send(e, null, (err, data) => { - if (err) { - reject(err); - } else { - resolve(data); - } - }); - }); - - await send(new Error("offline error")); + await raygunClient.send(new Error("offline error")); const files = fs.readdirSync(cachePath); @@ -44,7 +33,7 @@ test("offline message storage and sending", async function (t) { t.equal(data.details.error.message, "offline error"); - await send(new Error("offline error 2")); + await raygunClient.send(new Error("offline error 2")); await promisify(raygunClient.online.bind(raygunClient))(); await testEnvironment.nextRequest(); @@ -73,18 +62,7 @@ test("batched offline message storage and sending", async function (t) { }, }); const raygunClient = testEnvironment.client; - const send = (e) => - new Promise((resolve, reject) => { - raygunClient.send(e, null, (err, data) => { - if (err) { - reject(err); - } else { - resolve(data); - } - }); - }); - - await send(new Error("offline error")); + await raygunClient.send(new Error("offline error")); const files = fs.readdirSync(cachePath); @@ -100,7 +78,7 @@ test("batched offline message storage and sending", async function (t) { t.equal(data.details.error.message, "offline error"); - await send(new Error("offline error 2")); + await raygunClient.send(new Error("offline error 2")); await promisify(raygunClient.online.bind(raygunClient))(); const batch = await testEnvironment.nextBatchRequest(); diff --git a/test/raygun_send_test.js b/test/raygun_send_test.js index 7ab340b..7fa9a6b 100644 --- a/test/raygun_send_test.js +++ b/test/raygun_send_test.js @@ -27,7 +27,7 @@ test("send basic", {}, function (t) { var client = new Raygun.Client().init({ apiKey: API_KEY, }); - client.send(new Error(), {}, function (response) { + client.sendWithCallback(new Error(), {}, function (response) { t.equal(response.statusCode, 202); t.end(); }); @@ -47,7 +47,7 @@ test("send complex", {}, function (t) { .setUser("callum@mindscape.co.nz") .setVersion("1.0.0.0"); - client.send(new Error(), {}, function (response) { + client.sendWithCallback(new Error(), {}, function (response) { t.equal(response.statusCode, 202); t.end(); }); @@ -72,7 +72,7 @@ test("send with inner error", {}, function (t) { var client = new Raygun.Client().init({ apiKey: API_KEY, }); - client.send(error, {}, function (response) { + client.sendWithCallback(error, {}, function (response) { t.equal(response.statusCode, 202); t.end(); }); @@ -95,7 +95,7 @@ test("send with verror", {}, function (t) { var client = new Raygun.Client().init({ apiKey: API_KEY, }); - client.send(error, {}, function (response) { + client.sendWithCallback(error, {}, function (response) { t.equal(response.statusCode, 202); t.end(); }); @@ -120,7 +120,7 @@ test("send with OnBeforeSend", {}, function (t) { return payload; }); - client.send(new Error(), {}, function () { + client.sendWithCallback(new Error(), {}, function () { t.equal(onBeforeSendCalled, true); t.end(); }); @@ -135,9 +135,9 @@ test("send with expressHandler custom data", function (t) { client.expressCustomData = function () { return { test: "data" }; }; - client._send = client.send; - client.send = function (err, data) { - client.send = client._send; + client._send = client.sendWithCallback; + client.sendWithCallback = function (err, data) { + client.sendWithCallback = client._send; t.equal(data.test, "data"); t.end(); }; @@ -155,7 +155,7 @@ test("check that tags get passed through", {}, function (t) { return payload; }); - client.send(new Error(), {}, function () { + client.sendWithCallback(new Error(), {}, function () { t.end(); }); }); @@ -169,7 +169,7 @@ test("check that tags get merged", {}, function (t) { return payload; }); - client.send( + client.sendWithCallback( new Error(), {}, function () {