@simplybusiness/services 0.1.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.
Files changed (96) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/cjs/data/scripts-mock.js +59 -0
  3. package/dist/cjs/data/scripts-mock.js.map +1 -0
  4. package/dist/cjs/index.js +20 -0
  5. package/dist/cjs/index.js.map +1 -0
  6. package/dist/cjs/mocks/eventDefinitions.js +46 -0
  7. package/dist/cjs/mocks/eventDefinitions.js.map +1 -0
  8. package/dist/cjs/services/airbrake/index.js +23 -0
  9. package/dist/cjs/services/airbrake/index.js.map +1 -0
  10. package/dist/cjs/services/index.js +25 -0
  11. package/dist/cjs/services/index.js.map +1 -0
  12. package/dist/cjs/services/snowplow/SnowplowContext.js +61 -0
  13. package/dist/cjs/services/snowplow/SnowplowContext.js.map +1 -0
  14. package/dist/cjs/services/snowplow/contexts.js +18 -0
  15. package/dist/cjs/services/snowplow/contexts.js.map +1 -0
  16. package/dist/cjs/services/snowplow/event-definitions.js +235 -0
  17. package/dist/cjs/services/snowplow/event-definitions.js.map +1 -0
  18. package/dist/cjs/services/snowplow/getSnowplowConfig.js +16 -0
  19. package/dist/cjs/services/snowplow/getSnowplowConfig.js.map +1 -0
  20. package/dist/cjs/services/snowplow/index.js +138 -0
  21. package/dist/cjs/services/snowplow/index.js.map +1 -0
  22. package/dist/cjs/services/snowplow/types.js +6 -0
  23. package/dist/cjs/services/snowplow/types.js.map +1 -0
  24. package/dist/cjs/tsconfig.tsbuildinfo +1 -0
  25. package/dist/cjs/utils/index.js +20 -0
  26. package/dist/cjs/utils/index.js.map +1 -0
  27. package/dist/cjs/utils/testUtils.js +43 -0
  28. package/dist/cjs/utils/testUtils.js.map +1 -0
  29. package/dist/cjs/utils/text.js +13 -0
  30. package/dist/cjs/utils/text.js.map +1 -0
  31. package/dist/esm/data/scripts-mock.js +49 -0
  32. package/dist/esm/data/scripts-mock.js.map +1 -0
  33. package/dist/esm/index.js +3 -0
  34. package/dist/esm/index.js.map +1 -0
  35. package/dist/esm/mocks/eventDefinitions.js +36 -0
  36. package/dist/esm/mocks/eventDefinitions.js.map +1 -0
  37. package/dist/esm/services/airbrake/index.js +13 -0
  38. package/dist/esm/services/airbrake/index.js.map +1 -0
  39. package/dist/esm/services/index.js +8 -0
  40. package/dist/esm/services/index.js.map +1 -0
  41. package/dist/esm/services/snowplow/SnowplowContext.js +43 -0
  42. package/dist/esm/services/snowplow/SnowplowContext.js.map +1 -0
  43. package/dist/esm/services/snowplow/contexts.js +8 -0
  44. package/dist/esm/services/snowplow/contexts.js.map +1 -0
  45. package/dist/esm/services/snowplow/event-definitions.js +240 -0
  46. package/dist/esm/services/snowplow/event-definitions.js.map +1 -0
  47. package/dist/esm/services/snowplow/getSnowplowConfig.js +7 -0
  48. package/dist/esm/services/snowplow/getSnowplowConfig.js.map +1 -0
  49. package/dist/esm/services/snowplow/index.js +133 -0
  50. package/dist/esm/services/snowplow/index.js.map +1 -0
  51. package/dist/esm/services/snowplow/types.js +3 -0
  52. package/dist/esm/services/snowplow/types.js.map +1 -0
  53. package/dist/esm/utils/index.js +3 -0
  54. package/dist/esm/utils/index.js.map +1 -0
  55. package/dist/esm/utils/testUtils.js +26 -0
  56. package/dist/esm/utils/testUtils.js.map +1 -0
  57. package/dist/esm/utils/text.js +3 -0
  58. package/dist/esm/utils/text.js.map +1 -0
  59. package/dist/types/data/scripts-mock.d.ts +44 -0
  60. package/dist/types/index.d.ts +1 -0
  61. package/dist/types/mocks/eventDefinitions.d.ts +32 -0
  62. package/dist/types/services/airbrake/index.d.ts +2 -0
  63. package/dist/types/services/index.d.ts +5 -0
  64. package/dist/types/services/snowplow/SnowplowContext.d.ts +14 -0
  65. package/dist/types/services/snowplow/SnowplowContext.test.d.ts +1 -0
  66. package/dist/types/services/snowplow/contexts.d.ts +3 -0
  67. package/dist/types/services/snowplow/contexts.test.d.ts +1 -0
  68. package/dist/types/services/snowplow/event-definitions.d.ts +18 -0
  69. package/dist/types/services/snowplow/getSnowplowConfig.d.ts +2 -0
  70. package/dist/types/services/snowplow/index.d.ts +29 -0
  71. package/dist/types/services/snowplow/index.test.d.ts +1 -0
  72. package/dist/types/services/snowplow/types.d.ts +37 -0
  73. package/dist/types/utils/index.d.ts +1 -0
  74. package/dist/types/utils/testUtils.d.ts +7 -0
  75. package/dist/types/utils/text.d.ts +1 -0
  76. package/dist/types/utils/text.test.d.ts +1 -0
  77. package/package.json +86 -0
  78. package/src/__mocks__/snowplowBrowserTrackerMock.js +6 -0
  79. package/src/data/scripts-mock.ts +44 -0
  80. package/src/index.tsx +1 -0
  81. package/src/mocks/eventDefinitions.ts +39 -0
  82. package/src/services/airbrake/index.ts +16 -0
  83. package/src/services/index.tsx +6 -0
  84. package/src/services/snowplow/SnowplowContext.test.tsx +32 -0
  85. package/src/services/snowplow/SnowplowContext.tsx +68 -0
  86. package/src/services/snowplow/contexts.test.ts +42 -0
  87. package/src/services/snowplow/contexts.ts +14 -0
  88. package/src/services/snowplow/event-definitions.ts +256 -0
  89. package/src/services/snowplow/getSnowplowConfig.ts +8 -0
  90. package/src/services/snowplow/index.test.ts +194 -0
  91. package/src/services/snowplow/index.ts +163 -0
  92. package/src/services/snowplow/types.ts +65 -0
  93. package/src/utils/index.ts +1 -0
  94. package/src/utils/testUtils.tsx +36 -0
  95. package/src/utils/text.test.ts +9 -0
  96. package/src/utils/text.ts +2 -0
@@ -0,0 +1,29 @@
1
+ import { SelfDescribingJson, StructuredEvent } from "@snowplow/browser-tracker";
2
+ import { EventDefinition, TrackingProps } from "./types";
3
+ export type FrontOfficeStructuredEvent = StructuredEvent & {
4
+ serviceChannelIdentifier: string;
5
+ };
6
+ /**
7
+ * This class is an abstraction which wraps Snowplow
8
+ * and exposes common methods with other services:
9
+ * - trackEvent : sends a standard payload
10
+ * - trackUnstructEvent : sends a payload for custom schema
11
+ */
12
+ export declare class Snowplow {
13
+ avalancheTrackerName: string;
14
+ bronzeAvalancheTrackerName: string;
15
+ pvAvalancheTrackerName: string;
16
+ uid: unknown;
17
+ trackPageView: boolean;
18
+ contexts: SelfDescribingJson<Record<string, unknown>>[];
19
+ eventHandlers: Record<string, (params?: Record<string, unknown>) => void>;
20
+ constructor(props?: TrackingProps);
21
+ setContexts(contexts: SelfDescribingJson<Record<string, unknown>>[]): this;
22
+ trackView(): this;
23
+ trackEvent(event: StructuredEvent): Promise<this>;
24
+ trackUnstructEvent(event: SelfDescribingJson<Record<string, unknown>>): Promise<this>;
25
+ addEventHandlers(eventDefinitions: EventDefinition[]): this;
26
+ private addEventHandler;
27
+ private removeEventHandler;
28
+ trigger(name: string, params?: Record<string, unknown>): this;
29
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ import { EventMethod, SelfDescribingJson, StructuredEvent } from "@snowplow/browser-tracker";
2
+ type BaseConfig = {
3
+ appId: string;
4
+ avalancheCollector: string;
5
+ eventMethod: EventMethod;
6
+ trackPageView: boolean;
7
+ includeGAContext: boolean;
8
+ uid?: string;
9
+ postPath?: string;
10
+ };
11
+ export type EnvConfig = BaseConfig & {
12
+ cookieDomain: Record<string, string>;
13
+ };
14
+ export type TrackingProps = BaseConfig & {
15
+ eventMethod: EventMethod;
16
+ cookieDomain?: string;
17
+ };
18
+ export type ChannelContext = {
19
+ schema: string;
20
+ data: Record<string, string | number>;
21
+ };
22
+ export type ChannelContexts = Record<string, ChannelContext>;
23
+ export type ArrayOneOrMore<T> = [T, ...T[]];
24
+ export type ParamsType = Record<"label" | "deviceType" | "category" | "product" | "title" | "label" | "name" | "fromValue" | "toValue", string>;
25
+ export type EventDefinition = {
26
+ name: string;
27
+ type: "structured" | "unstructured";
28
+ makePayload: (params?: Record<string, unknown>) => StructuredEvent | SelfDescribingJson<Record<string, unknown>>;
29
+ };
30
+ export interface PageDataProps extends Partial<Record<"scripts", Array<{
31
+ metadata: {
32
+ name: string;
33
+ };
34
+ props?: Record<string, unknown>;
35
+ }>>> {
36
+ }
37
+ export {};
@@ -0,0 +1 @@
1
+ export * from "./text";
@@ -0,0 +1,7 @@
1
+ import { RenderOptions } from "@testing-library/react";
2
+ import { ReactElement } from "react";
3
+ export declare const renderWithProviders: (ui: ReactElement, options?: RenderOptions) => import("@testing-library/react").RenderResult<typeof import("@testing-library/dom/types/queries"), HTMLElement, HTMLElement>;
4
+ export declare const mockLocation: ({ origin, pathname, }: {
5
+ origin?: string;
6
+ pathname?: string;
7
+ }) => Window & typeof globalThis;
@@ -0,0 +1 @@
1
+ export declare const snakeCase: (text?: string) => string;
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "@simplybusiness/services",
3
+ "license": "UNLICENSED",
4
+ "version": "0.1.1",
5
+ "description": "Internal library for services",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+ssh://git@github.com/simplybusiness/mobius.git"
9
+ },
10
+ "simplyBusiness": {
11
+ "publishToPublicNpm": true
12
+ },
13
+ "main": "dist/cjs/index.js",
14
+ "module": "dist/esm/index.js",
15
+ "types": "./dist/types/index.d.ts",
16
+ "files": [
17
+ "src",
18
+ "dist"
19
+ ],
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/types/index.d.ts",
23
+ "require": "./dist/cjs/index.js",
24
+ "import": "./dist/esm/index.js"
25
+ }
26
+ },
27
+ "scripts": {
28
+ "clean": "rm -rf dist",
29
+ "build": "yarn run -T turbo run turbo:build",
30
+ "prepack": "yarn run build",
31
+ "build:cjs": "cd src && swc --config-file ../../.swcrc . -d ../dist/cjs -C module.type=commonjs",
32
+ "build:esm": "cd src && swc --config-file ../../.swcrc . -d ../dist/esm -C module.type=es6",
33
+ "build:types": "tsc --emitDeclarationOnly",
34
+ "lint": "eslint --ext .tsx,.ts,.js,.jsx,.mjs .",
35
+ "test": "NODE_OPTIONS=--experimental-vm-modules jest",
36
+ "test:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --collect-coverage",
37
+ "check-types": "tsc --noEmit --pretty"
38
+ },
39
+ "sideEffects": false,
40
+ "peerDependencies": {
41
+ "react": "^18.2.0",
42
+ "react-dom": "^18.2.0"
43
+ },
44
+ "devDependencies": {
45
+ "@react-types/shared": "^3.23.1",
46
+ "@swc/cli": "^0.3.14",
47
+ "@swc/core": "^1.6.5",
48
+ "@swc/jest": "^0.2.36",
49
+ "@testing-library/dom": "^10.2.0",
50
+ "@testing-library/jest-dom": "6.4.6",
51
+ "@testing-library/react": "^16.0.0",
52
+ "@types/jest": "^29.5.12",
53
+ "@types/react": "^18.3.3",
54
+ "@types/react-dom": "^18.3.0",
55
+ "@typescript-eslint/eslint-plugin": "^7.14.1",
56
+ "@typescript-eslint/parser": "^7.14.1",
57
+ "eslint": "^8.57.0",
58
+ "eslint-config-airbnb": "^19.0.4",
59
+ "eslint-config-prettier": "^9.1.0",
60
+ "eslint-import-resolver-typescript": "^3.6.1",
61
+ "eslint-plugin-import": "^2.29.1",
62
+ "eslint-plugin-jsx-a11y": "^6.9.0",
63
+ "eslint-plugin-no-only-tests": "^3.1.0",
64
+ "eslint-plugin-prettier": "^5.1.3",
65
+ "eslint-plugin-react": "^7.34.3",
66
+ "eslint-plugin-react-hooks": "^4.6.2",
67
+ "identity-obj-proxy": "^3.0.0",
68
+ "jest": "^29.7.0",
69
+ "jest-environment-jsdom": "^29.7.0",
70
+ "prettier": "^3.3.2",
71
+ "react": "^18.3.1",
72
+ "react-dom": "^18.3.1",
73
+ "ts-jest": "^29.1.5",
74
+ "tslib": "^2.6.3",
75
+ "typescript": "^5.5.2"
76
+ },
77
+ "dependencies": {
78
+ "@airbrake/browser": "^2.1.8",
79
+ "@simplybusiness/mobius": "^4.13.0",
80
+ "@snowplow/browser-tracker": "^3.24.0",
81
+ "classnames": "^2.5.1"
82
+ },
83
+ "lint-staged": {
84
+ "*.{js,ts,jsx,tsx}": "eslint --ext .tsx,.ts,.js,.jsx,.mjs --fix"
85
+ }
86
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Mocking this module prevents requests being sent to
3
+ * Snowplow which causes CORS errors in the test enviornment.
4
+ */
5
+
6
+ jest.mock("@snowplow/browser-tracker");
@@ -0,0 +1,44 @@
1
+ export const pageData = {
2
+ scripts: [
3
+ {
4
+ metadata: { name: "snowplow" },
5
+ props: {
6
+ uid: "49a449d8aaa9dd58f90a623d4b9dbcae235bf92a",
7
+ cookieDomain: "",
8
+ // TODO: Change this url to "http://localhost:8000" for local development
9
+ avalancheCollector:
10
+ "https://snowplow-collector-staging.simplybusiness.com",
11
+ appId: "us-chopin",
12
+ includeGAContext: true,
13
+ eventMethod: "post",
14
+ postPath: "/com.simplybusiness/events",
15
+ trackActivity: true,
16
+ trackPageView: true,
17
+ pageViewContext: {
18
+ schema: "igluuk.co.simplybusiness/journey_context/jsonschema/1-0-0",
19
+ data: {
20
+ site: "simplybusiness_us",
21
+ vertical: "usa",
22
+ super_segment: "Unknown",
23
+ primary_detail: "Lawn care services",
24
+ journey_name: "usa",
25
+ journey_id: "666ff79d90abbc3582e496da",
26
+ page_step_name: "thank_you_ssr",
27
+ page_step_depth: -1,
28
+ },
29
+ },
30
+ distributionChannelContext: {
31
+ schema:
32
+ "iglucom.simplybusiness/distribution_channel_context/jsonschema/1-0-0",
33
+ data: { service_channel_identifier: "simplybusiness_us" },
34
+ },
35
+ serviceChannelContext: {
36
+ schema:
37
+ "iglucom.simplybusiness/service_channel_context/jsonschema/1-0-0",
38
+ data: { service_channel_identifier: "simplybusiness_us" },
39
+ },
40
+ forceSecureTracker: true,
41
+ },
42
+ },
43
+ ],
44
+ };
package/src/index.tsx ADDED
@@ -0,0 +1 @@
1
+ export * from "./services";
@@ -0,0 +1,39 @@
1
+ export default [
2
+ {
3
+ name: "navButtonClicked",
4
+ type: "structured",
5
+ makePayload: (params: { label: "next" | "back" | "redirect" }) => {
6
+ const { label } = params;
7
+ return {
8
+ category: "navigation",
9
+ action: "thankyou_navigation_button_click",
10
+ label,
11
+ property: "test-property",
12
+ };
13
+ },
14
+ },
15
+ {
16
+ name: "questionAnswered",
17
+ type: "unstructured",
18
+ makePayload: (params: {
19
+ vertical?: string;
20
+ question: string;
21
+ answer?: string;
22
+ }) => {
23
+ const { vertical, question, answer } = params;
24
+ return {
25
+ schema:
26
+ "iglu:com.simplybusiness/form_question_answered/jsonschema/1-0-1",
27
+ data: {
28
+ site: "",
29
+ vertical: vertical || "business",
30
+ page_index: 1,
31
+ page_name: "Coverage diagnosis questionnaire",
32
+ section_name: "Coverage diagnosis questionnaire",
33
+ question,
34
+ answer,
35
+ },
36
+ };
37
+ },
38
+ },
39
+ ];
@@ -0,0 +1,16 @@
1
+ // Set Airbrake configuration for embedded-qcp project
2
+
3
+ import { Notifier } from "@airbrake/browser";
4
+
5
+ const getEnvironment = () =>
6
+ process.env.NODE_ENV === "test" ? "test" : "development";
7
+
8
+ // https://simplybusiness.airbrake.io/projects/512949/edit#tab-access
9
+ const projectId = 512949;
10
+ const projectKey = "4e25197d8faea61c10fbb97702200780";
11
+
12
+ export const airbrake = new Notifier({
13
+ projectId: +projectId,
14
+ projectKey,
15
+ environment: getEnvironment(),
16
+ });
@@ -0,0 +1,6 @@
1
+ // TODO: move all Snowplow related code to a single index.tsx file
2
+ export * from "./snowplow";
3
+ export * from "./snowplow/SnowplowContext";
4
+ export * from "./snowplow/getSnowplowConfig";
5
+ export * from "./snowplow/contexts";
6
+ export * from "./airbrake";
@@ -0,0 +1,32 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { pageData } from "../../data/scripts-mock";
3
+ import { getSnowplowConfig } from "./getSnowplowConfig";
4
+ import { SnowplowProvider } from "./SnowplowContext";
5
+ import { PageDataProps } from "./types";
6
+
7
+ const pageDataWithoutScripts = {
8
+ ...pageData,
9
+ scripts: [],
10
+ };
11
+
12
+ const snowplowProps = getSnowplowConfig(
13
+ pageDataWithoutScripts as PageDataProps,
14
+ );
15
+
16
+ describe("SnowplowProvider", () => {
17
+ describe("given pagaData has empty scripts entry", () => {
18
+ it("should render children without Snowplow instance errors", () => {
19
+ const text = "Example";
20
+
21
+ render(
22
+ <SnowplowProvider scripts={snowplowProps!}>
23
+ <p>{text}</p>
24
+ </SnowplowProvider>,
25
+ );
26
+
27
+ const textElement = screen.getByText(text);
28
+
29
+ expect(textElement).toBeInTheDocument();
30
+ });
31
+ });
32
+ });
@@ -0,0 +1,68 @@
1
+ /* eslint-disable @typescript-eslint/no-unused-vars */
2
+ import {
3
+ createContext,
4
+ useContext,
5
+ useEffect,
6
+ useMemo,
7
+ useState,
8
+ type ReactNode,
9
+ } from "react";
10
+ import { Snowplow } from ".";
11
+ import { getContexts } from "./contexts";
12
+ import { eventDefinitions } from "./event-definitions";
13
+ import { EventDefinition, TrackingProps } from "./types";
14
+
15
+ export interface SnowplowContextInterface {
16
+ config: TrackingProps;
17
+ snowplow?: Snowplow;
18
+ }
19
+
20
+ const SnowplowContext = createContext<SnowplowContextInterface | null>(null);
21
+
22
+ type ProviderProps = {
23
+ scripts: TrackingProps;
24
+ children: ReactNode;
25
+ };
26
+
27
+ export const SnowplowProvider = ({ scripts, children }: ProviderProps) => {
28
+ const [config, _setConfig] = useState<TrackingProps>(scripts);
29
+ const [snowplow, setSnowplow] = useState<Snowplow>(new Snowplow(config));
30
+
31
+ // Attach event handlers and set contexts
32
+ useEffect(() => {
33
+ if (snowplow && scripts) {
34
+ const contexts = getContexts(config);
35
+
36
+ snowplow
37
+ .setContexts(contexts)
38
+ .addEventHandlers(eventDefinitions as EventDefinition[]);
39
+ // Send page view event
40
+ if (config.trackPageView) snowplow.trackView();
41
+ }
42
+ }, [config, snowplow, scripts]);
43
+
44
+ const value: SnowplowContextInterface = useMemo(
45
+ () => ({
46
+ config,
47
+ snowplow,
48
+ }),
49
+ [config, snowplow],
50
+ );
51
+
52
+ return (
53
+ <SnowplowContext.Provider value={value}>
54
+ {children}
55
+ </SnowplowContext.Provider>
56
+ );
57
+ };
58
+
59
+ export function useSnowplowContext() {
60
+ const context = useContext(SnowplowContext);
61
+
62
+ if (!context) {
63
+ throw new Error(
64
+ "useSnowplowContext must be used inside a `SnowplowProvider`",
65
+ );
66
+ }
67
+ return context;
68
+ }
@@ -0,0 +1,42 @@
1
+ import { pageData } from "../../data/scripts-mock";
2
+ import { getContexts } from "./contexts";
3
+ import { getSnowplowConfig } from "./getSnowplowConfig";
4
+ import { PageDataProps, TrackingProps } from "./types";
5
+
6
+ describe("Snowplow Contexts", () => {
7
+ it("should extract all context records from snowplow props", () => {
8
+ const snowplowProps = getSnowplowConfig(pageData as PageDataProps);
9
+ const contexts = getContexts(snowplowProps as TrackingProps);
10
+
11
+ expect(contexts).toHaveLength(3);
12
+ expect(contexts[0]).toEqual({
13
+ pageViewContext: {
14
+ schema: "igluuk.co.simplybusiness/journey_context/jsonschema/1-0-0",
15
+ data: {
16
+ site: "simplybusiness_us",
17
+ vertical: "usa",
18
+ super_segment: "Unknown",
19
+ primary_detail: "Lawn care services",
20
+ journey_name: "usa",
21
+ journey_id: "666ff79d90abbc3582e496da",
22
+ page_step_name: "thank_you_ssr",
23
+ page_step_depth: -1,
24
+ },
25
+ },
26
+ });
27
+ expect(contexts[1]).toEqual({
28
+ distributionChannelContext: {
29
+ schema:
30
+ "iglucom.simplybusiness/distribution_channel_context/jsonschema/1-0-0",
31
+ data: { service_channel_identifier: "simplybusiness_us" },
32
+ },
33
+ });
34
+ expect(contexts[2]).toEqual({
35
+ serviceChannelContext: {
36
+ schema:
37
+ "iglucom.simplybusiness/service_channel_context/jsonschema/1-0-0",
38
+ data: { service_channel_identifier: "simplybusiness_us" },
39
+ },
40
+ });
41
+ });
42
+ });
@@ -0,0 +1,14 @@
1
+ import { SelfDescribingJson } from "@snowplow/browser-tracker";
2
+ import { TrackingProps } from "./types";
3
+
4
+ export const getContexts = (
5
+ config: TrackingProps,
6
+ ): SelfDescribingJson<Record<string, unknown>>[] => {
7
+ const contexts =
8
+ config &&
9
+ Object.entries(config)
10
+ .filter(([key]) => key.includes("Context") && key !== "includeGAContext")
11
+ .map(([key, value]) => ({ [key]: value }));
12
+
13
+ return contexts as unknown as SelfDescribingJson<Record<string, unknown>>[];
14
+ };