@chirpier/chirpier-js 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.
package/LICENCE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) Chirpier
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # Chirpier SDK
2
+
3
+ The Chirpier SDK is a lightweight, versatile library for monitoring and tracking streams of data in both browser and server environments. With built-in retry logic, and offline handling, the Chirpier SDK makes it easy to collect and send data to the Chirpier API.
4
+
5
+ ## Features
6
+
7
+ - Environment Agnostic: Works seamlessly in both browser and Node.js environments.
8
+ - Retry Logic: Includes retry mechanisms with exponential backoff for failed requests.
9
+ - Offline Support: Queues events when offline and sends them when the connection is restored.
10
+ - Easy Integration: Simple API for quick integration into your projects.
11
+
12
+ ## Installation
13
+
14
+ You can install the Chirpier SDK via npm:
15
+ ```
16
+ npm install @chirpier/sdk
17
+ ```
18
+
19
+ ## Getting Started
20
+
21
+ ### Initializing the SDK
22
+
23
+ To start using the SDK, you need to initialize it with your API key. The SDK works in both browser and Node.js environments.
24
+
25
+ #### In a Browser
26
+ ```
27
+ import { initialize, monitor } from '@chirpier/sdk-js';
28
+
29
+ // Initialize the SDK with your API key
30
+ initialize({ key: 'your-api-key' });
31
+
32
+ // Send a data stream tied to a group of streams
33
+ monitor({
34
+ group_id: '02e4f4d8-415e-4fc1-b01a-677ac5bc9207',
35
+ stream: 'Sales',
36
+ value: 15.30,
37
+ });
38
+ ```
39
+
40
+ #### In a Server (e.g., Express.js)
41
+ ```
42
+ const express = require('express');
43
+ const { initialize, monitor } = require('@chirpier/sdk-js');
44
+
45
+ const app = express();
46
+ const port = 3000;
47
+
48
+ // Initialize the SDK with your API key
49
+ initialize({ key: 'your-api-key' });
50
+
51
+ app.use(express.json());
52
+
53
+ app.post('/track-event', (req, res) => {
54
+ const { group_id, monitor, value } = req.body;
55
+
56
+ if (!group_id || !monitor || !value) {
57
+ return res.status(400).json({ error: 'Missing required fields' });
58
+ }
59
+
60
+ // Monitor an event
61
+ monitor({ group_id, monitor, value });
62
+
63
+ res.status(200).json({ message: 'Event tracked successfully' });
64
+ });
65
+
66
+ app.listen(port, () => {
67
+ console.log(`Server is running on http://localhost:${port}`);
68
+ });
69
+ ```
70
+
71
+ ## Example
72
+ ```
73
+ // Initialize the SDK with your API key
74
+ initialize({ key: 'your-api-key' });
75
+
76
+ // Monitor an event
77
+ monitor({
78
+ group_id: 'group UUID',
79
+ stream: 'Sales',
80
+ value: 15.3,
81
+ });
82
+ ```
83
+
84
+ ## Advanced Usage
85
+ Handling Offline Scenarios
86
+
87
+ The SDK automatically queues events when the network is unavailable and sends them when the connection is restored.
88
+
89
+ ## Custom Storage Mechanisms
90
+
91
+ The SDK uses localStorage for browser environments and in memory storage for Node.js. If you need a custom storage mechanism, you can extend the SDK by implementing the Storage interface.
@@ -0,0 +1,2 @@
1
+ export declare function cleanupMockServer(): void;
2
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../src/__tests__/mocks/server.ts"],"names":[],"mappings":"AAGA,wBAAgB,iBAAiB,SAEhC"}
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.cleanupMockServer = void 0;
7
+ var axios_1 = __importDefault(require("axios"));
8
+ // Cleanup mock server after tests
9
+ function cleanupMockServer() {
10
+ axios_1.default.defaults.adapter = undefined;
11
+ }
12
+ exports.cleanupMockServer = cleanupMockServer;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=sdk.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sdk.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/sdk.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ /**
3
+ * @jest-environment jsdom
4
+ */
5
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
6
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
7
+ return new (P || (P = Promise))(function (resolve, reject) {
8
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
9
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
10
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
11
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
12
+ });
13
+ };
14
+ var __generator = (this && this.__generator) || function (thisArg, body) {
15
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
16
+ return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
17
+ function verb(n) { return function (v) { return step([n, v]); }; }
18
+ function step(op) {
19
+ if (f) throw new TypeError("Generator is already executing.");
20
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
21
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
22
+ if (y = 0, t) op = [op[0] & 2, t.value];
23
+ switch (op[0]) {
24
+ case 0: case 1: t = op; break;
25
+ case 4: _.label++; return { value: op[1], done: false };
26
+ case 5: _.label++; y = op[1]; op = [0]; continue;
27
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
28
+ default:
29
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
30
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
31
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
32
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
33
+ if (t[2]) _.ops.pop();
34
+ _.trys.pop(); continue;
35
+ }
36
+ op = body.call(thisArg, _);
37
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
38
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
39
+ }
40
+ };
41
+ var __importDefault = (this && this.__importDefault) || function (mod) {
42
+ return (mod && mod.__esModule) ? mod : { "default": mod };
43
+ };
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ var index_1 = require("../index");
46
+ var constants_1 = require("../constants");
47
+ var axios_mock_adapter_1 = __importDefault(require("axios-mock-adapter"));
48
+ var axios_1 = __importDefault(require("axios"));
49
+ var server_1 = require("./mocks/server");
50
+ describe("Chirpier SDK", function () {
51
+ var chirpier;
52
+ afterEach(function () {
53
+ // Clean up mock server
54
+ (0, server_1.cleanupMockServer)();
55
+ });
56
+ describe("Initialization", function () {
57
+ test("should initialize with default values", function () {
58
+ chirpier = new index_1.Chirpier({
59
+ key: "api_key",
60
+ });
61
+ // Setup mock server
62
+ var mock = new axios_mock_adapter_1.default(axios_1.default);
63
+ mock.onPost(constants_1.DEFAULT_API_ENDPOINT).reply(200, { success: true });
64
+ expect(chirpier["apiEndpoint"]).toBe(constants_1.DEFAULT_API_ENDPOINT);
65
+ expect(chirpier["retries"]).toBe(constants_1.DEFAULT_RETRIES);
66
+ });
67
+ test("should initialize with custom values using mock server", function () {
68
+ chirpier = new index_1.Chirpier({
69
+ key: "api_key",
70
+ });
71
+ // Setup mock server
72
+ var mock = new axios_mock_adapter_1.default(axios_1.default);
73
+ mock.onPost(constants_1.DEFAULT_API_ENDPOINT).reply(200, { success: true });
74
+ var customChirpier = new index_1.Chirpier({
75
+ key: "api_key",
76
+ apiEndpoint: constants_1.DEFAULT_API_ENDPOINT,
77
+ retries: 5,
78
+ });
79
+ expect(customChirpier["apiEndpoint"]).toBe(constants_1.DEFAULT_API_ENDPOINT);
80
+ expect(customChirpier["retries"]).toBe(5);
81
+ });
82
+ test("should throw error if key is not provided", function () {
83
+ chirpier = new index_1.Chirpier({
84
+ key: "api_key",
85
+ });
86
+ // Setup mock server
87
+ var mock = new axios_mock_adapter_1.default(axios_1.default);
88
+ mock.onPost(constants_1.DEFAULT_API_ENDPOINT).reply(200, { success: true });
89
+ expect(function () { return new index_1.Chirpier({}); }).toThrow(index_1.ChirpierError);
90
+ });
91
+ });
92
+ describe("monitor", function () {
93
+ test("event should be sent", function () { return __awaiter(void 0, void 0, void 0, function () {
94
+ var mock, event;
95
+ return __generator(this, function (_a) {
96
+ switch (_a.label) {
97
+ case 0:
98
+ chirpier = new index_1.Chirpier({
99
+ key: "api_key",
100
+ });
101
+ mock = new axios_mock_adapter_1.default(axios_1.default);
102
+ mock.onPost(constants_1.DEFAULT_API_ENDPOINT).reply(200, { success: true });
103
+ event = {
104
+ group_id: "f3438ee9-b964-48aa-b938-a803df440a3c",
105
+ stream: "test-stream",
106
+ value: 1,
107
+ };
108
+ return [4 /*yield*/, chirpier.monitor(event)];
109
+ case 1:
110
+ _a.sent();
111
+ // Verify that the event was sent successfully
112
+ expect(mock.history.post.length).toBe(1);
113
+ expect(mock.history.post[0].url).toBe(constants_1.DEFAULT_API_ENDPOINT);
114
+ expect(JSON.parse(mock.history.post[0].data)).toEqual([
115
+ {
116
+ group_id: "f3438ee9-b964-48aa-b938-a803df440a3c",
117
+ stream: "test-stream",
118
+ value: 1,
119
+ event_id: expect.any(String),
120
+ },
121
+ ]);
122
+ // Clean up the mock
123
+ mock.reset();
124
+ return [2 /*return*/];
125
+ }
126
+ });
127
+ }); });
128
+ // Setup mock server
129
+ var mock = new axios_mock_adapter_1.default(axios_1.default);
130
+ mock.onPost(constants_1.DEFAULT_API_ENDPOINT).reply(200, { success: true });
131
+ test("should throw error for invalid event", function () { return __awaiter(void 0, void 0, void 0, function () {
132
+ var invalidEvent;
133
+ return __generator(this, function (_a) {
134
+ switch (_a.label) {
135
+ case 0:
136
+ chirpier = new index_1.Chirpier({
137
+ key: "api_key",
138
+ });
139
+ invalidEvent = {
140
+ group_id: "f3438ee9-b964-48aa-b938-a803df440a3c",
141
+ };
142
+ return [4 /*yield*/, expect(chirpier.monitor(invalidEvent)).rejects.toThrow(index_1.ChirpierError)];
143
+ case 1:
144
+ _a.sent();
145
+ // Clean up the mock
146
+ mock.reset();
147
+ return [2 /*return*/];
148
+ }
149
+ });
150
+ }); });
151
+ });
152
+ });
@@ -0,0 +1,4 @@
1
+ export declare const DEFAULT_API_ENDPOINT = "https://events.chirpier.co/api/events";
2
+ export declare const DEFAULT_RETRIES = 30;
3
+ export declare const DEFAULT_TIMEOUT = 5000;
4
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,oBAAoB,0CAA0C,CAAC;AAC5E,eAAO,MAAM,eAAe,KAAK,CAAC;AAClC,eAAO,MAAM,eAAe,OAAO,CAAA"}
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ // constants.ts
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.DEFAULT_TIMEOUT = exports.DEFAULT_RETRIES = exports.DEFAULT_API_ENDPOINT = void 0;
5
+ exports.DEFAULT_API_ENDPOINT = 'https://events.chirpier.co/api/events';
6
+ exports.DEFAULT_RETRIES = 30;
7
+ exports.DEFAULT_TIMEOUT = 5000;
@@ -0,0 +1,63 @@
1
+ interface Options {
2
+ key: string;
3
+ apiEndpoint?: string;
4
+ retries?: number;
5
+ timeout?: number;
6
+ }
7
+ export interface Event {
8
+ group_id: string;
9
+ stream: string;
10
+ value: number;
11
+ event_id?: string;
12
+ }
13
+ export declare class ChirpierError extends Error {
14
+ constructor(message: string);
15
+ }
16
+ /**
17
+ * Main Chirpier class for monitoring events.
18
+ */
19
+ export declare class Chirpier {
20
+ private readonly apiKey;
21
+ private readonly apiEndpoint;
22
+ private readonly retries;
23
+ private readonly timeout;
24
+ private readonly axiosInstance;
25
+ /**
26
+ * Initializes a new instance of the Chirpier class.
27
+ * @param options - Configuration options for the SDK.
28
+ */
29
+ constructor({ key, apiEndpoint, retries, timeout, }: Options);
30
+ /**
31
+ * Validates the event structure.
32
+ * @param event - The event to validate.
33
+ * @returns True if valid, false otherwise.
34
+ */
35
+ private isValidEvent;
36
+ /**
37
+ * Monitors an event by sending it to the API or storing it for retry.
38
+ * @param event - The event to monitor.
39
+ */
40
+ monitor(event: Event): Promise<void>;
41
+ /**
42
+ * Sends an event to the API.
43
+ * @param event - The event to send.
44
+ */
45
+ private sendEvent;
46
+ /**
47
+ * Sends multiple events to the API in a batch.
48
+ * @param events - The array of events to send.
49
+ */
50
+ private sendEvents;
51
+ }
52
+ /**
53
+ * Initializes the Chirpier SDK.
54
+ * @param options - Configuration options for the SDK.
55
+ */
56
+ export declare function initialize(options: Options): void;
57
+ /**
58
+ * Monitors an event using the Chirpier SDK.
59
+ * @param event - The event to monitor.
60
+ */
61
+ export declare function monitor(event: Event): void;
62
+ export {};
63
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAYA,UAAU,OAAO;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAGD,MAAM,WAAW,KAAK;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAGD,qBAAa,aAAc,SAAQ,KAAK;gBAC1B,OAAO,EAAE,MAAM;CAK5B;AAED;;GAEG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAE9C;;;OAGG;gBACS,EACV,GAAG,EACH,WAAkC,EAClC,OAAyB,EACzB,OAAyB,GAC1B,EAAE,OAAO;IAuCV;;;;OAIG;IACH,OAAO,CAAC,YAAY;IAUpB;;;OAGG;IACU,OAAO,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBjD;;;OAGG;YACW,SAAS;IAIvB;;;OAGG;YACW,UAAU;CAGzB;AAwCD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAkBjD;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAS1C"}
package/dist/index.js ADDED
@@ -0,0 +1,274 @@
1
+ "use strict";
2
+ var __extends = (this && this.__extends) || (function () {
3
+ var extendStatics = function (d, b) {
4
+ extendStatics = Object.setPrototypeOf ||
5
+ ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
6
+ function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
7
+ return extendStatics(d, b);
8
+ };
9
+ return function (d, b) {
10
+ if (typeof b !== "function" && b !== null)
11
+ throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
12
+ extendStatics(d, b);
13
+ function __() { this.constructor = d; }
14
+ d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
15
+ };
16
+ })();
17
+ var __assign = (this && this.__assign) || function () {
18
+ __assign = Object.assign || function(t) {
19
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
20
+ s = arguments[i];
21
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
22
+ t[p] = s[p];
23
+ }
24
+ return t;
25
+ };
26
+ return __assign.apply(this, arguments);
27
+ };
28
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
29
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
30
+ return new (P || (P = Promise))(function (resolve, reject) {
31
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
32
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
33
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
34
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
35
+ });
36
+ };
37
+ var __generator = (this && this.__generator) || function (thisArg, body) {
38
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
39
+ return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
40
+ function verb(n) { return function (v) { return step([n, v]); }; }
41
+ function step(op) {
42
+ if (f) throw new TypeError("Generator is already executing.");
43
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
44
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
45
+ if (y = 0, t) op = [op[0] & 2, t.value];
46
+ switch (op[0]) {
47
+ case 0: case 1: t = op; break;
48
+ case 4: _.label++; return { value: op[1], done: false };
49
+ case 5: _.label++; y = op[1]; op = [0]; continue;
50
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
51
+ default:
52
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
53
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
54
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
55
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
56
+ if (t[2]) _.ops.pop();
57
+ _.trys.pop(); continue;
58
+ }
59
+ op = body.call(thisArg, _);
60
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
61
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
62
+ }
63
+ };
64
+ var __importDefault = (this && this.__importDefault) || function (mod) {
65
+ return (mod && mod.__esModule) ? mod : { "default": mod };
66
+ };
67
+ Object.defineProperty(exports, "__esModule", { value: true });
68
+ exports.monitor = exports.initialize = exports.Chirpier = exports.ChirpierError = void 0;
69
+ // Import necessary dependencies
70
+ var axios_1 = __importDefault(require("axios"));
71
+ var axios_retry_1 = __importDefault(require("axios-retry"));
72
+ var uuid_1 = require("@lukeed/uuid");
73
+ var js_base64_1 = require("js-base64");
74
+ var constants_1 = require("./constants");
75
+ // Custom error class for Chirpier-specific errors
76
+ var ChirpierError = /** @class */ (function (_super) {
77
+ __extends(ChirpierError, _super);
78
+ function ChirpierError(message) {
79
+ var _this = _super.call(this, message) || this;
80
+ _this.name = "ChirpierError";
81
+ Object.setPrototypeOf(_this, ChirpierError.prototype);
82
+ return _this;
83
+ }
84
+ return ChirpierError;
85
+ }(Error));
86
+ exports.ChirpierError = ChirpierError;
87
+ /**
88
+ * Main Chirpier class for monitoring events.
89
+ */
90
+ var Chirpier = /** @class */ (function () {
91
+ /**
92
+ * Initializes a new instance of the Chirpier class.
93
+ * @param options - Configuration options for the SDK.
94
+ */
95
+ function Chirpier(_a) {
96
+ var key = _a.key, _b = _a.apiEndpoint, apiEndpoint = _b === void 0 ? constants_1.DEFAULT_API_ENDPOINT : _b, _c = _a.retries, retries = _c === void 0 ? constants_1.DEFAULT_RETRIES : _c, _d = _a.timeout, timeout = _d === void 0 ? constants_1.DEFAULT_TIMEOUT : _d;
97
+ if (!key || typeof key !== "string") {
98
+ throw new ChirpierError("API key is required and must be a string");
99
+ }
100
+ this.apiKey = key;
101
+ this.apiEndpoint = apiEndpoint;
102
+ this.retries = retries;
103
+ this.timeout = timeout;
104
+ // Create axios instance with authorization header
105
+ this.axiosInstance = axios_1.default.create({
106
+ headers: { Authorization: "Bearer ".concat(this.apiKey) },
107
+ timeout: this.timeout,
108
+ });
109
+ // Add the interceptor here
110
+ this.axiosInstance.interceptors.response.use(function (response) { return response; }, function (error) {
111
+ // Don't handle the error here; let axios-retry handle it
112
+ return Promise.reject(error);
113
+ });
114
+ // Apply axios-retry to your Axios instance
115
+ (0, axios_retry_1.default)(this.axiosInstance, {
116
+ retries: this.retries,
117
+ retryDelay: function (retryCount, error) {
118
+ return Math.pow(2, retryCount) * 2000; // Exponential backoff starting at 1 second
119
+ },
120
+ retryCondition: function (error) {
121
+ return (axios_retry_1.default.isNetworkError(error) || axios_retry_1.default.isRetryableError(error));
122
+ },
123
+ shouldResetTimeout: true,
124
+ });
125
+ }
126
+ /**
127
+ * Validates the event structure.
128
+ * @param event - The event to validate.
129
+ * @returns True if valid, false otherwise.
130
+ */
131
+ Chirpier.prototype.isValidEvent = function (event) {
132
+ return (typeof event.group_id === "string" && /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(event.group_id) &&
133
+ event.group_id.trim().length > 0 &&
134
+ typeof event.stream === "string" &&
135
+ event.stream.trim().length > 0 &&
136
+ typeof event.value === "number");
137
+ };
138
+ /**
139
+ * Monitors an event by sending it to the API or storing it for retry.
140
+ * @param event - The event to monitor.
141
+ */
142
+ Chirpier.prototype.monitor = function (event) {
143
+ return __awaiter(this, void 0, void 0, function () {
144
+ var eventWithID, error_1;
145
+ return __generator(this, function (_a) {
146
+ switch (_a.label) {
147
+ case 0:
148
+ if (!this.isValidEvent(event)) {
149
+ throw new ChirpierError("Invalid event format. Must include group_id, stream, and numeric value.");
150
+ }
151
+ eventWithID = __assign(__assign({}, event), { event_id: event.event_id || (0, uuid_1.v4)() });
152
+ _a.label = 1;
153
+ case 1:
154
+ _a.trys.push([1, 3, , 4]);
155
+ return [4 /*yield*/, this.sendEvent(eventWithID)];
156
+ case 2:
157
+ _a.sent();
158
+ console.info("Event successfully sent:", eventWithID.event_id);
159
+ return [3 /*break*/, 4];
160
+ case 3:
161
+ error_1 = _a.sent();
162
+ console.error("Failed to send event after retries:", error_1);
163
+ return [3 /*break*/, 4];
164
+ case 4: return [2 /*return*/];
165
+ }
166
+ });
167
+ });
168
+ };
169
+ /**
170
+ * Sends an event to the API.
171
+ * @param event - The event to send.
172
+ */
173
+ Chirpier.prototype.sendEvent = function (event) {
174
+ return __awaiter(this, void 0, void 0, function () {
175
+ return __generator(this, function (_a) {
176
+ switch (_a.label) {
177
+ case 0: return [4 /*yield*/, this.sendEvents([event])];
178
+ case 1:
179
+ _a.sent();
180
+ return [2 /*return*/];
181
+ }
182
+ });
183
+ });
184
+ };
185
+ /**
186
+ * Sends multiple events to the API in a batch.
187
+ * @param events - The array of events to send.
188
+ */
189
+ Chirpier.prototype.sendEvents = function (events) {
190
+ return __awaiter(this, void 0, void 0, function () {
191
+ return __generator(this, function (_a) {
192
+ switch (_a.label) {
193
+ case 0: return [4 /*yield*/, this.axiosInstance.post(this.apiEndpoint, events)];
194
+ case 1:
195
+ _a.sent();
196
+ return [2 /*return*/];
197
+ }
198
+ });
199
+ });
200
+ };
201
+ return Chirpier;
202
+ }());
203
+ exports.Chirpier = Chirpier;
204
+ /**
205
+ * Decodes a base64url encoded string.
206
+ * @param str - The base64url encoded string to decode.
207
+ * @returns The decoded string.
208
+ */
209
+ function base64UrlDecode(str) {
210
+ // Replace '-' with '+' and '_' with '/'
211
+ var base64 = str.replace(/-/g, "+").replace(/_/g, "/");
212
+ // Pad the base64 string
213
+ var padding = base64.length % 4;
214
+ if (padding !== 0) {
215
+ base64 += "=".repeat(4 - padding);
216
+ }
217
+ return js_base64_1.Base64.decode(base64);
218
+ }
219
+ /**
220
+ * Validates if the provided token is a valid JWT.
221
+ * @param token - The token to validate.
222
+ * @returns True if valid, false otherwise.
223
+ */
224
+ function isValidJWT(token) {
225
+ var parts = token.split(".");
226
+ if (parts.length !== 3) {
227
+ return false;
228
+ }
229
+ try {
230
+ var header = JSON.parse(base64UrlDecode(parts[0]));
231
+ var payload = JSON.parse(base64UrlDecode(parts[1]));
232
+ return typeof header === "object" && typeof payload === "object";
233
+ }
234
+ catch (error) {
235
+ return false;
236
+ }
237
+ }
238
+ // Singleton instance of Chirpier
239
+ var chirpierInstance = null;
240
+ /**
241
+ * Initializes the Chirpier SDK.
242
+ * @param options - Configuration options for the SDK.
243
+ */
244
+ function initialize(options) {
245
+ if (!isValidJWT(options.key)) {
246
+ throw new ChirpierError("Invalid API key: Not a valid JWT");
247
+ }
248
+ try {
249
+ chirpierInstance = new Chirpier(options);
250
+ }
251
+ catch (error) {
252
+ if (error instanceof ChirpierError) {
253
+ console.error("Failed to initialize Chirpier SDK:", error.message);
254
+ }
255
+ else {
256
+ console.error("An unexpected error occurred during Chirpier SDK initialization:", error);
257
+ }
258
+ throw error;
259
+ }
260
+ }
261
+ exports.initialize = initialize;
262
+ /**
263
+ * Monitors an event using the Chirpier SDK.
264
+ * @param event - The event to monitor.
265
+ */
266
+ function monitor(event) {
267
+ if (!chirpierInstance) {
268
+ throw new ChirpierError("Chirpier SDK is not initialized. Please call initialize() first.");
269
+ }
270
+ chirpierInstance.monitor(event).catch(function (error) {
271
+ console.error("Error in monitor function:", error);
272
+ });
273
+ }
274
+ exports.monitor = monitor;
@@ -0,0 +1,5 @@
1
+ export declare class LocalStorageStorage {
2
+ save(events: any[]): void;
3
+ load(): any[];
4
+ }
5
+ //# sourceMappingURL=storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA,qBAAa,mBAAmB;IAC5B,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,IAAI;IAIzB,IAAI,IAAI,GAAG,EAAE;CAId"}
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LocalStorageStorage = void 0;
4
+ var LocalStorageStorage = /** @class */ (function () {
5
+ function LocalStorageStorage() {
6
+ }
7
+ LocalStorageStorage.prototype.save = function (events) {
8
+ localStorage.setItem('chirpier_events', JSON.stringify(events));
9
+ };
10
+ LocalStorageStorage.prototype.load = function () {
11
+ var storedEvents = localStorage.getItem('chirpier_events');
12
+ return storedEvents ? JSON.parse(storedEvents) : [];
13
+ };
14
+ return LocalStorageStorage;
15
+ }());
16
+ exports.LocalStorageStorage = LocalStorageStorage;
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@chirpier/chirpier-js",
3
+ "version": "0.1.1",
4
+ "description": "Chirpier SDK for JavaScript",
5
+ "keywords": [
6
+ "chirpier",
7
+ "sdk",
8
+ "javascript",
9
+ "monitoring",
10
+ "event-tracking"
11
+ ],
12
+ "author": "Chirpier",
13
+ "license": "MIT",
14
+ "main": "dist/index.js",
15
+ "types": "dist/index.d.ts",
16
+ "files": [
17
+ "dist/**/*",
18
+ "src/**/*",
19
+ "types/**/*",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsc",
25
+ "prepublishOnly": "npm run build && npm test",
26
+ "lint": "eslint 'src/**/*.ts'",
27
+ "test": "jest --coverage"
28
+ },
29
+ "dependencies": {
30
+ "@lukeed/uuid": "^2.0.1",
31
+ "axios": "^0.24.0",
32
+ "axios-retry": "^4.5.0",
33
+ "jest-environment-jsdom": "^29.7.0",
34
+ "js-base64": "^3.7.7",
35
+ "ts-node": "^10.9.2",
36
+ "tslib": "^2.3.0"
37
+ },
38
+ "devDependencies": {
39
+ "@jest/globals": "^29.7.0",
40
+ "@types/jest": "^29.5.13",
41
+ "@types/mocha": "^10.0.8",
42
+ "@types/node": "^22.5.0",
43
+ "@types/uuid": "^10.0.0",
44
+ "axios-mock-adapter": "^2.0.0",
45
+ "jest": "^29.7.0",
46
+ "ts-jest": "^29.2.4",
47
+ "typescript": "^4.4.3",
48
+ "webpack": "^5.52.0",
49
+ "webpack-cli": "^4.8.0"
50
+ }
51
+ }
@@ -0,0 +1,6 @@
1
+ import axios from "axios";
2
+
3
+ // Cleanup mock server after tests
4
+ export function cleanupMockServer() {
5
+ axios.defaults.adapter = undefined;
6
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ import { Chirpier, ChirpierError, Event } from "../index";
6
+ import { DEFAULT_API_ENDPOINT, DEFAULT_RETRIES } from "../constants";
7
+ import MockAdapter from "axios-mock-adapter";
8
+ import axios from "axios";
9
+ import { cleanupMockServer } from "./mocks/server";
10
+
11
+ describe("Chirpier SDK", () => {
12
+ let chirpier: Chirpier;
13
+
14
+ afterEach(() => {
15
+ // Clean up mock server
16
+ cleanupMockServer();
17
+ });
18
+
19
+ describe("Initialization", () => {
20
+ test("should initialize with default values", () => {
21
+ chirpier = new Chirpier({
22
+ key: "api_key",
23
+ });
24
+
25
+ // Setup mock server
26
+ const mock = new MockAdapter(axios);
27
+ mock.onPost(DEFAULT_API_ENDPOINT).reply(200, { success: true });
28
+
29
+ expect(chirpier["apiEndpoint"]).toBe(DEFAULT_API_ENDPOINT);
30
+ expect(chirpier["retries"]).toBe(DEFAULT_RETRIES);
31
+ });
32
+
33
+ test("should initialize with custom values using mock server", () => {
34
+ chirpier = new Chirpier({
35
+ key: "api_key",
36
+ });
37
+
38
+ // Setup mock server
39
+ const mock = new MockAdapter(axios);
40
+ mock.onPost(DEFAULT_API_ENDPOINT).reply(200, { success: true });
41
+
42
+ const customChirpier = new Chirpier({
43
+ key: "api_key",
44
+ apiEndpoint: DEFAULT_API_ENDPOINT,
45
+ retries: 5,
46
+ });
47
+
48
+ expect(customChirpier["apiEndpoint"]).toBe(DEFAULT_API_ENDPOINT);
49
+ expect(customChirpier["retries"]).toBe(5);
50
+ });
51
+
52
+ test("should throw error if key is not provided", () => {
53
+ chirpier = new Chirpier({
54
+ key: "api_key",
55
+ });
56
+
57
+ // Setup mock server
58
+ const mock = new MockAdapter(axios);
59
+ mock.onPost(DEFAULT_API_ENDPOINT).reply(200, { success: true });
60
+
61
+ expect(() => new Chirpier({} as any)).toThrow(ChirpierError);
62
+ });
63
+ });
64
+
65
+ describe("monitor", () => {
66
+ test("event should be sent", async () => {
67
+ chirpier = new Chirpier({
68
+ key: "api_key",
69
+ });
70
+
71
+ // Setup mock server
72
+ const mock = new MockAdapter(axios);
73
+ mock.onPost(DEFAULT_API_ENDPOINT).reply(200, { success: true });
74
+
75
+ const event: Event = {
76
+ group_id: "f3438ee9-b964-48aa-b938-a803df440a3c",
77
+ stream: "test-stream",
78
+ value: 1,
79
+ };
80
+
81
+ await chirpier.monitor(event);
82
+
83
+ // Verify that the event was sent successfully
84
+ expect(mock.history.post.length).toBe(1);
85
+ expect(mock.history.post[0].url).toBe(DEFAULT_API_ENDPOINT);
86
+ expect(JSON.parse(mock.history.post[0].data)).toEqual([
87
+ {
88
+ group_id: "f3438ee9-b964-48aa-b938-a803df440a3c",
89
+ stream: "test-stream",
90
+ value: 1,
91
+ event_id: expect.any(String),
92
+ },
93
+ ]);
94
+
95
+ // Clean up the mock
96
+ mock.reset();
97
+ });
98
+
99
+ // Setup mock server
100
+ const mock = new MockAdapter(axios);
101
+ mock.onPost(DEFAULT_API_ENDPOINT).reply(200, { success: true });
102
+
103
+ test("should throw error for invalid event", async () => {
104
+ chirpier = new Chirpier({
105
+ key: "api_key",
106
+ });
107
+ const invalidEvent = {
108
+ group_id: "f3438ee9-b964-48aa-b938-a803df440a3c",
109
+ } as any;
110
+ await expect(chirpier.monitor(invalidEvent)).rejects.toThrow(
111
+ ChirpierError
112
+ );
113
+
114
+ // Clean up the mock
115
+ mock.reset();
116
+ });
117
+ });
118
+ });
@@ -0,0 +1,5 @@
1
+ // constants.ts
2
+
3
+ export const DEFAULT_API_ENDPOINT = 'https://events.chirpier.co/api/events';
4
+ export const DEFAULT_RETRIES = 30;
5
+ export const DEFAULT_TIMEOUT = 5000
package/src/index.ts ADDED
@@ -0,0 +1,224 @@
1
+ // Import necessary dependencies
2
+ import axios, { AxiosInstance } from "axios";
3
+ import axiosRetry from "axios-retry";
4
+ import { v4 as uuidv4 } from "@lukeed/uuid";
5
+ import { Base64 } from 'js-base64';
6
+ import {
7
+ DEFAULT_API_ENDPOINT,
8
+ DEFAULT_RETRIES,
9
+ DEFAULT_TIMEOUT,
10
+ } from "./constants";
11
+
12
+ // Define the options interface for Chirpier initialization
13
+ interface Options {
14
+ key: string;
15
+ apiEndpoint?: string;
16
+ retries?: number;
17
+ timeout?: number;
18
+ }
19
+
20
+ // Define the Event interface for monitoring
21
+ export interface Event {
22
+ group_id: string;
23
+ stream: string;
24
+ value: number;
25
+ event_id?: string;
26
+ }
27
+
28
+ // Custom error class for Chirpier-specific errors
29
+ export class ChirpierError extends Error {
30
+ constructor(message: string) {
31
+ super(message);
32
+ this.name = "ChirpierError";
33
+ Object.setPrototypeOf(this, ChirpierError.prototype);
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Main Chirpier class for monitoring events.
39
+ */
40
+ export class Chirpier {
41
+ private readonly apiKey: string;
42
+ private readonly apiEndpoint: string;
43
+ private readonly retries: number;
44
+ private readonly timeout: number;
45
+ private readonly axiosInstance: AxiosInstance;
46
+
47
+ /**
48
+ * Initializes a new instance of the Chirpier class.
49
+ * @param options - Configuration options for the SDK.
50
+ */
51
+ constructor({
52
+ key,
53
+ apiEndpoint = DEFAULT_API_ENDPOINT,
54
+ retries = DEFAULT_RETRIES,
55
+ timeout = DEFAULT_TIMEOUT,
56
+ }: Options) {
57
+ if (!key || typeof key !== "string") {
58
+ throw new ChirpierError("API key is required and must be a string");
59
+ }
60
+ this.apiKey = key;
61
+ this.apiEndpoint = apiEndpoint;
62
+ this.retries = retries;
63
+ this.timeout = timeout;
64
+
65
+ // Create axios instance with authorization header
66
+ this.axiosInstance = axios.create({
67
+ headers: { Authorization: `Bearer ${this.apiKey}` },
68
+ timeout: this.timeout,
69
+ });
70
+
71
+ // Add the interceptor here
72
+ this.axiosInstance.interceptors.response.use(
73
+ (response) => response,
74
+ (error) => {
75
+ // Don't handle the error here; let axios-retry handle it
76
+ return Promise.reject(error);
77
+ }
78
+ );
79
+
80
+ // Apply axios-retry to your Axios instance
81
+ axiosRetry(this.axiosInstance, {
82
+ retries: this.retries,
83
+ retryDelay: (retryCount, error) => {
84
+ return Math.pow(2, retryCount) * 2000; // Exponential backoff starting at 1 second
85
+ },
86
+ retryCondition: (error) => {
87
+ return (
88
+ axiosRetry.isNetworkError(error) || axiosRetry.isRetryableError(error)
89
+ );
90
+ },
91
+ shouldResetTimeout: true,
92
+ });
93
+ }
94
+
95
+ /**
96
+ * Validates the event structure.
97
+ * @param event - The event to validate.
98
+ * @returns True if valid, false otherwise.
99
+ */
100
+ private isValidEvent(event: Event): boolean {
101
+ return (
102
+ typeof event.group_id === "string" && /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(event.group_id) &&
103
+ event.group_id.trim().length > 0 &&
104
+ typeof event.stream === "string" &&
105
+ event.stream.trim().length > 0 &&
106
+ typeof event.value === "number"
107
+ );
108
+ }
109
+
110
+ /**
111
+ * Monitors an event by sending it to the API or storing it for retry.
112
+ * @param event - The event to monitor.
113
+ */
114
+ public async monitor(event: Event): Promise<void> {
115
+ if (!this.isValidEvent(event)) {
116
+ throw new ChirpierError(
117
+ "Invalid event format. Must include group_id, stream, and numeric value."
118
+ );
119
+ }
120
+
121
+ // Ensure event_id is only set once
122
+ const eventWithID = { ...event, event_id: event.event_id || uuidv4() };
123
+
124
+ try {
125
+ await this.sendEvent(eventWithID);
126
+ console.info("Event successfully sent:", eventWithID.event_id);
127
+ } catch (error) {
128
+ console.error("Failed to send event after retries:", error);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Sends an event to the API.
134
+ * @param event - The event to send.
135
+ */
136
+ private async sendEvent(event: Event): Promise<void> {
137
+ await this.sendEvents([event]);
138
+ }
139
+
140
+ /**
141
+ * Sends multiple events to the API in a batch.
142
+ * @param events - The array of events to send.
143
+ */
144
+ private async sendEvents(events: Event[]): Promise<void> {
145
+ await this.axiosInstance.post(this.apiEndpoint, events);
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Decodes a base64url encoded string.
151
+ * @param str - The base64url encoded string to decode.
152
+ * @returns The decoded string.
153
+ */
154
+ function base64UrlDecode(str: string): string {
155
+ // Replace '-' with '+' and '_' with '/'
156
+ let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
157
+ // Pad the base64 string
158
+ const padding = base64.length % 4;
159
+ if (padding !== 0) {
160
+ base64 += "=".repeat(4 - padding);
161
+ }
162
+ return Base64.decode(base64);
163
+ }
164
+
165
+ /**
166
+ * Validates if the provided token is a valid JWT.
167
+ * @param token - The token to validate.
168
+ * @returns True if valid, false otherwise.
169
+ */
170
+ function isValidJWT(token: string): boolean {
171
+ const parts = token.split(".");
172
+ if (parts.length !== 3) {
173
+ return false;
174
+ }
175
+ try {
176
+ const header = JSON.parse(base64UrlDecode(parts[0]));
177
+ const payload = JSON.parse(base64UrlDecode(parts[1]));
178
+ return typeof header === "object" && typeof payload === "object";
179
+ } catch (error) {
180
+ return false;
181
+ }
182
+ }
183
+
184
+ // Singleton instance of Chirpier
185
+ let chirpierInstance: Chirpier | null = null;
186
+
187
+ /**
188
+ * Initializes the Chirpier SDK.
189
+ * @param options - Configuration options for the SDK.
190
+ */
191
+ export function initialize(options: Options): void {
192
+ if (!isValidJWT(options.key)) {
193
+ throw new ChirpierError("Invalid API key: Not a valid JWT");
194
+ }
195
+
196
+ try {
197
+ chirpierInstance = new Chirpier(options);
198
+ } catch (error) {
199
+ if (error instanceof ChirpierError) {
200
+ console.error("Failed to initialize Chirpier SDK:", error.message);
201
+ } else {
202
+ console.error(
203
+ "An unexpected error occurred during Chirpier SDK initialization:",
204
+ error
205
+ );
206
+ }
207
+ throw error;
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Monitors an event using the Chirpier SDK.
213
+ * @param event - The event to monitor.
214
+ */
215
+ export function monitor(event: Event): void {
216
+ if (!chirpierInstance) {
217
+ throw new ChirpierError(
218
+ "Chirpier SDK is not initialized. Please call initialize() first."
219
+ );
220
+ }
221
+ chirpierInstance.monitor(event).catch((error) => {
222
+ console.error("Error in monitor function:", error);
223
+ });
224
+ }
package/src/storage.ts ADDED
@@ -0,0 +1,10 @@
1
+ export class LocalStorageStorage {
2
+ save(events: any[]): void {
3
+ localStorage.setItem('chirpier_events', JSON.stringify(events));
4
+ }
5
+
6
+ load(): any[] {
7
+ const storedEvents = localStorage.getItem('chirpier_events');
8
+ return storedEvents ? JSON.parse(storedEvents) : [];
9
+ }
10
+ }