@simplybusiness/services 1.5.0 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/dist/cjs/index.js +48 -25
- package/dist/esm/index.js +48 -25
- package/dist/esm/tsconfig.build.tsbuildinfo +1 -1
- package/dist/types/src/airbrake/index.d.ts +2 -1
- package/package.json +3 -3
- package/src/airbrake/index.test.ts +127 -0
- package/src/airbrake/index.ts +28 -22
- package/src/snowplow/event-definitions/questionnaire/questionnaire.test.ts +129 -0
- package/src/snowplow/event-definitions/questionnaire/questionnaire.ts +37 -7
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
export declare const getAirbrakeEnvironment: () => string;
|
|
1
2
|
export type GetAirbrakeOptions = {
|
|
2
3
|
projectId?: number;
|
|
3
4
|
projectKey?: string;
|
|
4
5
|
forceSend?: boolean;
|
|
5
6
|
};
|
|
6
|
-
export declare function getAirbrake(options
|
|
7
|
+
export declare function getAirbrake(options?: GetAirbrakeOptions): {
|
|
7
8
|
notify: jest.Mock<any, any, any> | {
|
|
8
9
|
(...data: any[]): void;
|
|
9
10
|
(message?: any, ...optionalParams: any[]): void;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simplybusiness/services",
|
|
3
3
|
"license": "UNLICENSED",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.6.1",
|
|
5
5
|
"description": "Internal library for services",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"@eslint/compat": "^2.0.0",
|
|
47
47
|
"@eslint/eslintrc": "^3.3.1",
|
|
48
48
|
"@eslint/js": "^9.39.1",
|
|
49
|
-
"@simplybusiness/eslint-config": "^1.1.
|
|
49
|
+
"@simplybusiness/eslint-config": "^1.1.1",
|
|
50
50
|
"@swc/core": "^1.12.5",
|
|
51
51
|
"@swc/jest": "^0.2.39",
|
|
52
52
|
"@testing-library/dom": "^10.4.1",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
},
|
|
80
80
|
"dependencies": {
|
|
81
81
|
"@airbrake/browser": "^2.1.9",
|
|
82
|
-
"@simplybusiness/mobius": "^6.9.
|
|
82
|
+
"@simplybusiness/mobius": "^6.9.7",
|
|
83
83
|
"@snowplow/browser-tracker": "^3.24.6",
|
|
84
84
|
"classnames": "^2.5.1"
|
|
85
85
|
},
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Notifier } from "@airbrake/browser";
|
|
2
|
+
import { getAirbrake, getAirbrakeEnvironment } from "./index";
|
|
3
|
+
|
|
4
|
+
jest.mock("@airbrake/browser");
|
|
5
|
+
|
|
6
|
+
const MockNotifier = Notifier as jest.MockedClass<typeof Notifier>;
|
|
7
|
+
|
|
8
|
+
describe("getAirbrake", () => {
|
|
9
|
+
const originalEnv = process.env.AIRBRAKE_ENV;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
jest.clearAllMocks();
|
|
13
|
+
// Clear the notifier instances cache between tests
|
|
14
|
+
// by creating new notifiers with different projectIds
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
if (originalEnv === undefined) {
|
|
19
|
+
delete process.env.AIRBRAKE_ENV;
|
|
20
|
+
} else {
|
|
21
|
+
process.env.AIRBRAKE_ENV = originalEnv;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("getAirbrakeEnvironment", () => {
|
|
26
|
+
it("returns AIRBRAKE_ENV when set", () => {
|
|
27
|
+
process.env.AIRBRAKE_ENV = "staging";
|
|
28
|
+
expect(getAirbrakeEnvironment()).toBe("staging");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("returns 'development' when AIRBRAKE_ENV is not set", () => {
|
|
32
|
+
delete process.env.AIRBRAKE_ENV;
|
|
33
|
+
expect(getAirbrakeEnvironment()).toBe("development");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("environment-based routing", () => {
|
|
38
|
+
it("returns real notifier when AIRBRAKE_ENV is 'production'", () => {
|
|
39
|
+
process.env.AIRBRAKE_ENV = "production";
|
|
40
|
+
const result = getAirbrake({ projectId: 1001 });
|
|
41
|
+
|
|
42
|
+
expect(MockNotifier).toHaveBeenCalledWith(
|
|
43
|
+
expect.objectContaining({
|
|
44
|
+
projectId: 1001,
|
|
45
|
+
environment: "production",
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
48
|
+
expect(result).toBeInstanceOf(Notifier);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("returns real notifier when AIRBRAKE_ENV is 'staging'", () => {
|
|
52
|
+
process.env.AIRBRAKE_ENV = "staging";
|
|
53
|
+
const result = getAirbrake({ projectId: 1002 });
|
|
54
|
+
|
|
55
|
+
expect(MockNotifier).toHaveBeenCalledWith(
|
|
56
|
+
expect.objectContaining({
|
|
57
|
+
projectId: 1002,
|
|
58
|
+
environment: "staging",
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
expect(result).toBeInstanceOf(Notifier);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("returns fake notifier when AIRBRAKE_ENV is 'development'", () => {
|
|
65
|
+
process.env.AIRBRAKE_ENV = "development";
|
|
66
|
+
const result = getAirbrake({ projectId: 1003 });
|
|
67
|
+
|
|
68
|
+
expect(MockNotifier).not.toHaveBeenCalled();
|
|
69
|
+
// In test environment, notify is a jest.fn() to avoid console spam
|
|
70
|
+
expect(typeof result.notify).toBe("function");
|
|
71
|
+
expect(result).not.toBeInstanceOf(Notifier);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("returns fake notifier when AIRBRAKE_ENV is 'integration'", () => {
|
|
75
|
+
process.env.AIRBRAKE_ENV = "integration";
|
|
76
|
+
const result = getAirbrake({ projectId: 1004 });
|
|
77
|
+
|
|
78
|
+
expect(MockNotifier).not.toHaveBeenCalled();
|
|
79
|
+
// In test environment, notify is a jest.fn() to avoid console spam
|
|
80
|
+
expect(typeof result.notify).toBe("function");
|
|
81
|
+
expect(result).not.toBeInstanceOf(Notifier);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("forceSend option", () => {
|
|
86
|
+
it("returns real notifier when forceSend is true regardless of environment", () => {
|
|
87
|
+
process.env.AIRBRAKE_ENV = "development";
|
|
88
|
+
const result = getAirbrake({ projectId: 1006, forceSend: true });
|
|
89
|
+
|
|
90
|
+
expect(MockNotifier).toHaveBeenCalledWith(
|
|
91
|
+
expect.objectContaining({
|
|
92
|
+
projectId: 1006,
|
|
93
|
+
environment: "development",
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
expect(result).toBeInstanceOf(Notifier);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("multiple project support", () => {
|
|
101
|
+
it("returns separate notifiers for different projectIds", () => {
|
|
102
|
+
process.env.AIRBRAKE_ENV = "production";
|
|
103
|
+
|
|
104
|
+
const result1 = getAirbrake({ projectId: 2001, projectKey: "key1" });
|
|
105
|
+
const result2 = getAirbrake({ projectId: 2002, projectKey: "key2" });
|
|
106
|
+
|
|
107
|
+
expect(MockNotifier).toHaveBeenCalledTimes(2);
|
|
108
|
+
expect(MockNotifier).toHaveBeenCalledWith(
|
|
109
|
+
expect.objectContaining({ projectId: 2001 }),
|
|
110
|
+
);
|
|
111
|
+
expect(MockNotifier).toHaveBeenCalledWith(
|
|
112
|
+
expect.objectContaining({ projectId: 2002 }),
|
|
113
|
+
);
|
|
114
|
+
expect(result1).not.toBe(result2);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("returns cached notifier for same projectId", () => {
|
|
118
|
+
process.env.AIRBRAKE_ENV = "production";
|
|
119
|
+
|
|
120
|
+
const result1 = getAirbrake({ projectId: 3001 });
|
|
121
|
+
const result2 = getAirbrake({ projectId: 3001 });
|
|
122
|
+
|
|
123
|
+
expect(MockNotifier).toHaveBeenCalledTimes(1);
|
|
124
|
+
expect(result1).toBe(result2);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
package/src/airbrake/index.ts
CHANGED
|
@@ -5,27 +5,25 @@ import { Notifier } from "@airbrake/browser";
|
|
|
5
5
|
const defaultProjectId = 512949;
|
|
6
6
|
const defaultProjectKey = "4e25197d8faea61c10fbb97702200780";
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
// Use jest.fn() for testing
|
|
11
|
-
if (process.env.NODE_ENV === "test") {
|
|
12
|
-
return jest.fn();
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
return console.error;
|
|
16
|
-
};
|
|
8
|
+
// Map of project IDs to notifier instances to support multiple Airbrake accounts
|
|
9
|
+
const notifierInstances = new Map<number, Notifier>();
|
|
17
10
|
|
|
18
|
-
// Fake
|
|
11
|
+
// Fake notifier which logs to console instead of sending to Airbrake
|
|
19
12
|
const fakeAirbrake = {
|
|
20
|
-
notify:
|
|
13
|
+
notify:
|
|
14
|
+
globalThis.process?.env?.NODE_ENV === "test"
|
|
15
|
+
? jest.fn()
|
|
16
|
+
: console.error.bind(console),
|
|
21
17
|
};
|
|
22
18
|
|
|
23
|
-
//
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
const isTest = process.env.NODE_ENV === "test";
|
|
19
|
+
// Get Airbrake environment from AIRBRAKE_ENV, defaulting to "development"
|
|
20
|
+
export const getAirbrakeEnvironment = () =>
|
|
21
|
+
globalThis.process?.env?.AIRBRAKE_ENV ?? "development";
|
|
27
22
|
|
|
28
|
-
|
|
23
|
+
// Only send real alerts in staging and production environments
|
|
24
|
+
const shouldUseAirbrake = () => {
|
|
25
|
+
const env = getAirbrakeEnvironment();
|
|
26
|
+
return env === "staging" || env === "production";
|
|
29
27
|
};
|
|
30
28
|
|
|
31
29
|
export type GetAirbrakeOptions = {
|
|
@@ -34,22 +32,30 @@ export type GetAirbrakeOptions = {
|
|
|
34
32
|
forceSend?: boolean;
|
|
35
33
|
};
|
|
36
34
|
|
|
37
|
-
export function getAirbrake(options: GetAirbrakeOptions) {
|
|
38
|
-
const {
|
|
35
|
+
export function getAirbrake(options: GetAirbrakeOptions = {}) {
|
|
36
|
+
const {
|
|
37
|
+
projectId = defaultProjectId,
|
|
38
|
+
projectKey = defaultProjectKey,
|
|
39
|
+
forceSend,
|
|
40
|
+
} = options;
|
|
39
41
|
|
|
40
|
-
if (!forceSend
|
|
42
|
+
if (!forceSend && !shouldUseAirbrake()) {
|
|
41
43
|
return fakeAirbrake;
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
let notifierInstance = notifierInstances.get(projectId);
|
|
47
|
+
|
|
44
48
|
if (!notifierInstance) {
|
|
45
49
|
notifierInstance = new Notifier({
|
|
46
|
-
projectId
|
|
47
|
-
projectKey
|
|
48
|
-
environment:
|
|
50
|
+
projectId,
|
|
51
|
+
projectKey,
|
|
52
|
+
environment: getAirbrakeEnvironment(),
|
|
49
53
|
instrumentation: {
|
|
50
54
|
onerror: false,
|
|
55
|
+
unhandledrejection: false,
|
|
51
56
|
},
|
|
52
57
|
});
|
|
58
|
+
notifierInstances.set(projectId, notifierInstance);
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
return notifierInstance;
|
|
@@ -124,6 +124,7 @@ describe("questionnaireEventDefinitions", () => {
|
|
|
124
124
|
vertical: "shop",
|
|
125
125
|
primary_detail: "test-answer",
|
|
126
126
|
selected_type: "trade_selector",
|
|
127
|
+
location: "/",
|
|
127
128
|
},
|
|
128
129
|
});
|
|
129
130
|
});
|
|
@@ -143,6 +144,134 @@ describe("questionnaireEventDefinitions", () => {
|
|
|
143
144
|
|
|
144
145
|
expect("data" in result && result.data.vertical).toBe("Landlord");
|
|
145
146
|
});
|
|
147
|
+
|
|
148
|
+
it("should include trade selection metadata for US page", () => {
|
|
149
|
+
const params = {
|
|
150
|
+
context: {
|
|
151
|
+
site: "simplybusiness_us",
|
|
152
|
+
vertical: "shop",
|
|
153
|
+
},
|
|
154
|
+
answer: "Plumber",
|
|
155
|
+
searchId: "search-123",
|
|
156
|
+
selectedListPosition: 2,
|
|
157
|
+
selectionMethod: "genAI",
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const result = findEventByName("primaryDetailSelected").makePayload(
|
|
161
|
+
params,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
expect(result).toEqual({
|
|
165
|
+
schema:
|
|
166
|
+
"iglu:com.simplybusiness/primary_detail_selected/jsonschema/1-4-0",
|
|
167
|
+
data: {
|
|
168
|
+
site: "simplybusiness_us",
|
|
169
|
+
vertical: "shop",
|
|
170
|
+
primary_detail: "Plumber",
|
|
171
|
+
selected_type: "trade_selector",
|
|
172
|
+
location: "/",
|
|
173
|
+
search_id: "search-123",
|
|
174
|
+
selected_list_position: "3",
|
|
175
|
+
selected_location: "trade_selector_vertical",
|
|
176
|
+
selection_method: "genAI",
|
|
177
|
+
business_unit: "simplybusiness_us",
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("should include search_id but not US-only fields for non-US page", () => {
|
|
183
|
+
const params = {
|
|
184
|
+
context: {
|
|
185
|
+
site: "simplybusiness_uk",
|
|
186
|
+
vertical: "shop",
|
|
187
|
+
},
|
|
188
|
+
answer: "Plumber",
|
|
189
|
+
searchId: "search-456",
|
|
190
|
+
selectedListPosition: 1,
|
|
191
|
+
selectionMethod: "legacy",
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const result = findEventByName("primaryDetailSelected").makePayload(
|
|
195
|
+
params,
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
expect(result).toEqual({
|
|
199
|
+
schema:
|
|
200
|
+
"iglu:com.simplybusiness/primary_detail_selected/jsonschema/1-4-0",
|
|
201
|
+
data: {
|
|
202
|
+
site: "simplybusiness_uk",
|
|
203
|
+
vertical: "shop",
|
|
204
|
+
primary_detail: "Plumber",
|
|
205
|
+
selected_type: "trade_selector",
|
|
206
|
+
location: "/",
|
|
207
|
+
search_id: "search-456",
|
|
208
|
+
selection_method: "legacy",
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("should handle selectedListPosition of 0 (first item)", () => {
|
|
214
|
+
const params = {
|
|
215
|
+
context: {
|
|
216
|
+
site: "simplybusiness_us",
|
|
217
|
+
vertical: "shop",
|
|
218
|
+
},
|
|
219
|
+
answer: "Plumber",
|
|
220
|
+
searchId: "search-789",
|
|
221
|
+
selectedListPosition: 0,
|
|
222
|
+
selectionMethod: "genAI",
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const result = findEventByName("primaryDetailSelected").makePayload(
|
|
226
|
+
params,
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
expect(result).toEqual({
|
|
230
|
+
schema:
|
|
231
|
+
"iglu:com.simplybusiness/primary_detail_selected/jsonschema/1-4-0",
|
|
232
|
+
data: {
|
|
233
|
+
site: "simplybusiness_us",
|
|
234
|
+
vertical: "shop",
|
|
235
|
+
primary_detail: "Plumber",
|
|
236
|
+
selected_type: "trade_selector",
|
|
237
|
+
location: "/",
|
|
238
|
+
search_id: "search-789",
|
|
239
|
+
selected_list_position: "1",
|
|
240
|
+
selected_location: "trade_selector_vertical",
|
|
241
|
+
selection_method: "genAI",
|
|
242
|
+
business_unit: "simplybusiness_us",
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("should include US-only fields with null position when no metadata", () => {
|
|
248
|
+
const params = {
|
|
249
|
+
context: {
|
|
250
|
+
site: "simplybusiness_us",
|
|
251
|
+
vertical: "shop",
|
|
252
|
+
},
|
|
253
|
+
answer: "Electrician",
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const result = findEventByName("primaryDetailSelected").makePayload(
|
|
257
|
+
params,
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
expect(result).toEqual({
|
|
261
|
+
schema:
|
|
262
|
+
"iglu:com.simplybusiness/primary_detail_selected/jsonschema/1-4-0",
|
|
263
|
+
data: {
|
|
264
|
+
site: "simplybusiness_us",
|
|
265
|
+
vertical: "shop",
|
|
266
|
+
primary_detail: "Electrician",
|
|
267
|
+
selected_type: "trade_selector",
|
|
268
|
+
location: "/",
|
|
269
|
+
selected_list_position: null,
|
|
270
|
+
selected_location: "trade_selector_vertical",
|
|
271
|
+
business_unit: "simplybusiness_us",
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
});
|
|
146
275
|
});
|
|
147
276
|
|
|
148
277
|
describe("helpTextOpened", () => {
|
|
@@ -35,8 +35,16 @@ export const questionnaireEventDefinitions: EventDefinition[] = [
|
|
|
35
35
|
name: "primaryDetailSelected",
|
|
36
36
|
type: "unstructured",
|
|
37
37
|
makePayload: params => {
|
|
38
|
-
const {
|
|
38
|
+
const {
|
|
39
|
+
context,
|
|
40
|
+
answer,
|
|
41
|
+
vertical,
|
|
42
|
+
searchId,
|
|
43
|
+
selectedListPosition,
|
|
44
|
+
selectionMethod,
|
|
45
|
+
} = params as ParamsType;
|
|
39
46
|
const { site } = context;
|
|
47
|
+
const isUSPage = site === "simplybusiness_us";
|
|
40
48
|
let verticalName = vertical || context.vertical;
|
|
41
49
|
|
|
42
50
|
if (verticalName.toLowerCase().indexOf("landlord") > -1) {
|
|
@@ -44,15 +52,37 @@ export const questionnaireEventDefinitions: EventDefinition[] = [
|
|
|
44
52
|
answer === "residential" ? "Landlord" : "Commercial landlord";
|
|
45
53
|
}
|
|
46
54
|
|
|
55
|
+
const data: Record<string, unknown> = {
|
|
56
|
+
site,
|
|
57
|
+
vertical: verticalName,
|
|
58
|
+
primary_detail: answer,
|
|
59
|
+
selected_type: "trade_selector",
|
|
60
|
+
location: window?.location?.pathname ?? "",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Always include search_id if present
|
|
64
|
+
if (searchId) {
|
|
65
|
+
data.search_id = searchId;
|
|
66
|
+
}
|
|
67
|
+
// Always include selection_method if present
|
|
68
|
+
if (selectionMethod) {
|
|
69
|
+
data.selection_method = selectionMethod;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// US only fields
|
|
73
|
+
if (isUSPage) {
|
|
74
|
+
data.selected_list_position =
|
|
75
|
+
selectedListPosition !== undefined
|
|
76
|
+
? (selectedListPosition + 1).toString()
|
|
77
|
+
: null;
|
|
78
|
+
data.selected_location = "trade_selector_vertical";
|
|
79
|
+
data.business_unit = site;
|
|
80
|
+
}
|
|
81
|
+
|
|
47
82
|
return {
|
|
48
83
|
schema:
|
|
49
84
|
"iglu:com.simplybusiness/primary_detail_selected/jsonschema/1-4-0",
|
|
50
|
-
data
|
|
51
|
-
site,
|
|
52
|
-
vertical: verticalName,
|
|
53
|
-
primary_detail: answer,
|
|
54
|
-
selected_type: "trade_selector",
|
|
55
|
-
},
|
|
85
|
+
data,
|
|
56
86
|
};
|
|
57
87
|
},
|
|
58
88
|
contexts: ["distributionChannelContext", "serviceChannelContext"],
|