Skip to content

Commit e737a7f

Browse files
authored
Add callback argument to sendError and wrap (#514)
Currently the `sendError` functions (and in turn the `wrap` helper) has `tags` and `namespace` parameters. We've seen in the past that this is quite limiting for the `sendError` helper as users can't add additional metadata like an action name, parameters, etc. Deprecate the `tags` and `namespace` parameters in favor of the new callback function. The deprecations will log a warning to the console when it's detected they are used. I've not disabled their functionality so users can upgrade at their own pace. I'm not very familiar with TypeScript definitions so I've done my best with what I saw in the rest of the library and found in the docs. The `tags` parameter in the `send` and `sendError` functions now serves the double purpose of `tags` parameter and callback function. I've done my best to differentiate between these two function signatures by defining multiple signatures. The thing that was most unclear to me was passing the multi purpose parameter as a HashMap to the `setTags` function. I ended up casting it to a HashMap with the `toHashMap` function I saw used elsewhere, only here I also needed to cast it explicitly to a `HashMap`, hence the import. Part of appsignal/integration-guide#41 We also need to update the docs.
1 parent 387ee27 commit e737a7f

File tree

5 files changed

+174
-16
lines changed

5 files changed

+174
-16
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
bump: "patch"
3+
---
4+
5+
Add callback argument to the `sendError` function to allow for more customization of errors sent with `sendError` and `wrap`. The `tags` and `namespace` parameters are now deprecated for both helpers.
6+
7+
```js
8+
// Deprecated behavior
9+
appsignal.sendError(
10+
new Error("sendError with tags and namespace argument"),
11+
{ tag1: "value 1", tag2: "value 2" },
12+
"custom_namespace"
13+
);
14+
15+
// New behavior
16+
appsignal.sendError(
17+
new Error("sendError with callback argument"),
18+
function(span) {
19+
span.setAction("SendErrorTestAction");
20+
span.setNamespace("custom_namespace");
21+
span.setTags({ tag1: "value 1", tag2: "value 2" });
22+
}
23+
);
24+
```

packages/javascript/src/__tests__/index.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,22 @@ describe("Appsignal", () => {
7474
expect(promise).resolves
7575
})
7676

77+
it("modifies the span before pushing the error", async () => {
78+
const message = "test error"
79+
let resultSpan: Span | undefined
80+
const promise = appsignal.sendError(new Error(message), function (span) {
81+
span.setAction("CustomAction")
82+
resultSpan = span
83+
})
84+
85+
await expect(promise).resolves
86+
if (resultSpan) {
87+
expect(resultSpan.serialize()?.action).toBe("CustomAction")
88+
} else {
89+
throw new Error("No resultSpan found!")
90+
}
91+
})
92+
7793
it("doesn't send an invalid error", () => {
7894
const spy = jest.spyOn(console, "error").mockImplementation()
7995

@@ -156,6 +172,28 @@ describe("Appsignal", () => {
156172

157173
expect(promise).rejects
158174
})
175+
176+
it("modifies the span before pushing the error", async () => {
177+
let resultSpan: Span | undefined
178+
const promise = appsignal
179+
.wrap(
180+
() => {
181+
throw new Error("test error")
182+
},
183+
function (span) {
184+
span.setAction("CustomAction")
185+
resultSpan = span
186+
}
187+
)
188+
.catch(e => expect(e.message).toEqual("test error"))
189+
190+
await expect(promise).rejects
191+
if (resultSpan) {
192+
expect(resultSpan.serialize()?.action).toBe("CustomAction")
193+
} else {
194+
throw new Error("No resultSpan found!")
195+
}
196+
})
159197
})
160198

161199
describe("createSpan", () => {

packages/javascript/src/index.ts

Lines changed: 73 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import { compose, toHashMap } from "@appsignal/core"
7-
import type { Breadcrumb, JSClient, Hook } from "@appsignal/types"
7+
import type { Breadcrumb, JSClient, Hook, HashMap } from "@appsignal/types"
88

99
import { VERSION } from "./version"
1010
import { PushApi } from "./api"
@@ -70,6 +70,16 @@ export default class Appsignal implements JSClient {
7070
this._options = options
7171
}
7272

73+
/**
74+
* Records and sends a browser `Error` to AppSignal.
75+
*
76+
* @param {Error} error A JavaScript Error object
77+
* @param {Function | void} fn Optional callback function to modify span before it's sent.
78+
*
79+
* @return {Promise<Span> | void} An API response, or `void` if `Promise` is unsupported.
80+
*/
81+
public send<T>(error: Error, fn?: (span: Span) => T): Promise<Span> | void
82+
7383
/**
7484
* Records and sends a browser `Error` to AppSignal.
7585
*
@@ -97,14 +107,14 @@ export default class Appsignal implements JSClient {
97107
/**
98108
*
99109
* @param {Error | Span} data A JavaScript Error or Appsignal Span object
100-
* @param {object} tags An key, value object of tags
101-
* @param {string} namespace An optional namespace name
110+
* @param {object | Function} tagsOrFn An key-value object of tags or a callback function to customize the span before it is sent.
111+
* @param {string} namespace DEPRECATED: An optional namespace name.
102112
*
103113
* @return {Promise<Span> | void} An API response, or `void` if `Promise` is unsupported.
104114
*/
105-
public send(
115+
public send<T>(
106116
data: Error | Span,
107-
tags = {},
117+
tagsOrFn?: object | ((span: Span) => T),
108118
namespace?: string
109119
): Promise<any> | void {
110120
if (!(data instanceof Error) && !(data instanceof Span)) {
@@ -148,8 +158,25 @@ export default class Appsignal implements JSClient {
148158
compose(...this._hooks.decorators)(span)
149159
}
150160

151-
if (tags) span.setTags(tags)
152-
if (namespace) span.setNamespace(namespace)
161+
if (tagsOrFn) {
162+
if (typeof tagsOrFn === "function") {
163+
const callback = tagsOrFn
164+
callback(span)
165+
} else {
166+
console.warn(
167+
"[APPSIGNAL]: DEPRECATED: Calling the `send`/`sendError` function with a tags object is deprecated. Use the callback argument instead."
168+
)
169+
const tags = (toHashMap(tagsOrFn) || {}) as HashMap<string>
170+
span.setTags(tags)
171+
}
172+
}
173+
if (namespace) {
174+
console.warn(
175+
"[APPSIGNAL]: DEPRECATED: Calling the `send`/`sendError` function with a namespace is deprecated. Use the callback argument instead."
176+
)
177+
span.setNamespace(namespace)
178+
}
179+
153180
if (this._breadcrumbs.length > 0) span.setBreadcrumbs(this._breadcrumbs)
154181

155182
// A Span can be "overridden" with metadata after it has been created,
@@ -194,22 +221,30 @@ export default class Appsignal implements JSClient {
194221
}
195222
}
196223

224+
sendError<T>(error: Error): Promise<Span> | void
225+
sendError<T>(error: Error, callback: (span: Span) => T): Promise<Span> | void
226+
sendError<T>(
227+
error: Error,
228+
tags?: object,
229+
namespace?: string
230+
): Promise<Span> | void
231+
197232
/**
198233
* Records and sends a browser `Error` to AppSignal. An alias to `#send()`
199234
* to maintain compatibility.
200235
*
201236
* @param {Error} error A JavaScript Error object
202-
* @param {object} tags An key, value object of tags
203-
* @param {string} namespace An optional namespace name
237+
* @param {object | Function} tagsOrFn An key-value object of tags or callback function to customize the span before it is sent.
238+
* @param {string} namespace DEPRECATED: An optional namespace name
204239
*
205240
* @return {Promise<Span> | void} An API response, or `void` if `Promise` is unsupported.
206241
*/
207-
public sendError(
242+
public sendError<T>(
208243
error: Error,
209-
tags?: object,
244+
tagsOrFn?: object | ((span: Span) => T),
210245
namespace?: string
211246
): Promise<Span> | void {
212-
return this.send(error, tags, namespace)
247+
return this.send(error, tagsOrFn, namespace)
213248
}
214249

215250
/**
@@ -250,22 +285,44 @@ export default class Appsignal implements JSClient {
250285
return span
251286
}
252287

288+
/**
289+
* Wraps and catches errors within a given function. If the function throws an
290+
* error, a rejected `Promise` will be returned and the error thrown will be
291+
* logged to AppSignal.
292+
*/
293+
public async wrap<T>(fn: () => T, callbackFn?: (span: Span) => T): Promise<T>
294+
295+
/**
296+
* Wraps and catches errors within a given function. If the function throws an
297+
* error, a rejected `Promise` will be returned and the error thrown will be
298+
* logged to AppSignal.
299+
*/
300+
public async wrap<T>(
301+
fn: () => T,
302+
tags?: object,
303+
namespace?: string
304+
): Promise<T>
305+
253306
/**
254307
* Wraps and catches errors within a given function. If the function throws an
255308
* error, a rejected `Promise` will be returned and the error thrown will be
256309
* logged to AppSignal.
257310
*
258311
* @param {Function} fn A function to wrap
259-
* @param {object} tags An key, value object of tags
260-
* @param {string} namespace An optional namespace name
312+
* @param {object | Function} tagsOrFn An key-value object of tags or a callback function to customize the span before it is sent.
313+
* @param {string} namespace DEPRECATED: An optional namespace name
261314
*
262315
* @return {Promise<any>} A Promise containing the return value of the function, or a `Span` if an error was thrown.
263316
*/
264-
public async wrap<T>(fn: () => T, tags = {}, namespace?: string): Promise<T> {
317+
public async wrap<T>(
318+
fn: () => T,
319+
tagsOrFn?: object | ((span: Span) => T),
320+
namespace?: string
321+
): Promise<T> {
265322
try {
266323
return await fn()
267324
} catch (e) {
268-
await this.sendError(e, tags, namespace)
325+
await this.sendError(e, tagsOrFn, namespace)
269326
return Promise.reject(e)
270327
}
271328
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
bump: "patch"
3+
---
4+
5+
Add callback argument to the `sendError` function to allow for more customization of errors sent with `sendError` and `wrap`. The `tags` and `namespace` parameters are now deprecated for both helpers.
6+
7+
```js
8+
// Deprecated behavior
9+
appsignal.sendError(
10+
new Error("sendError with tags and namespace argument"),
11+
{ tag1: "value 1", tag2: "value 2" },
12+
"custom_namespace"
13+
);
14+
15+
// New behavior
16+
appsignal.sendError(
17+
new Error("sendError with callback argument"),
18+
function(span) {
19+
span.setAction("SendErrorTestAction");
20+
span.setNamespace("custom_namespace");
21+
span.setTags({ tag1: "value 1", tag2: "value 2" });
22+
}
23+
);
24+
```

packages/types/src/interfaces/client.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ export interface NodeClient extends BaseClient {
8787
export interface JSClient extends BaseClient {
8888
ignored: RegExp[]
8989

90+
/**
91+
* Records and sends a browser `Error` to AppSignal.
92+
*/
93+
send<T>(error: Error, fn?: (span: JSSpan) => T): Promise<JSSpan> | void
94+
9095
/**
9196
* Records and sends a browser `Error` to AppSignal.
9297
*/
@@ -103,6 +108,9 @@ export interface JSClient extends BaseClient {
103108
namespace?: string
104109
): Promise<any> | void
105110

111+
sendError<T>(error: Error): Promise<JSSpan> | void
112+
sendError<T>(error: Error, fn?: (span: JSSpan) => T): Promise<JSSpan> | void
113+
106114
/**
107115
* Records and sends a browser `Error` to AppSignal. An alias to `#send()`
108116
* to maintain compatibility.
@@ -127,6 +135,13 @@ export interface JSClient extends BaseClient {
127135
*/
128136
createSpan(fn?: (span: JSSpan) => void): JSSpan
129137

138+
/**
139+
* Wraps and catches errors within a given function. If the function throws an
140+
* error, a rejected `Promise` will be returned and the error thrown will be
141+
* logged to AppSignal.
142+
*/
143+
wrap<T>(fn: () => T, callbackFn?: (span: JSSpan) => T): Promise<T>
144+
130145
/**
131146
* Wraps and catches errors within a given function. If the function throws an
132147
* error, a rejected `Promise` will be returned and the error thrown will be

0 commit comments

Comments
 (0)