@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 +22 -0
- package/README.md +91 -0
- package/dist/__tests__/mocks/server.d.ts +2 -0
- package/dist/__tests__/mocks/server.d.ts.map +1 -0
- package/dist/__tests__/mocks/server.js +12 -0
- package/dist/__tests__/sdk.test.d.ts +5 -0
- package/dist/__tests__/sdk.test.d.ts.map +1 -0
- package/dist/__tests__/sdk.test.js +152 -0
- package/dist/constants.d.ts +4 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +7 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +274 -0
- package/dist/storage.d.ts +5 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +16 -0
- package/package.json +51 -0
- package/src/__tests__/mocks/server.ts +6 -0
- package/src/__tests__/sdk.test.ts +118 -0
- package/src/constants.ts +5 -0
- package/src/index.ts +224 -0
- package/src/storage.ts +10 -0
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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;
|
package/dist/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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"}
|
package/dist/storage.js
ADDED
|
@@ -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,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
|
+
});
|
package/src/constants.ts
ADDED
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
|
+
}
|