@notix-hub/sdk 0.1.0

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/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # @notix-hub/sdk
2
+
3
+ JavaScript SDK для **Notix (Нотикс)** — сервиса агрегации уведомлений.
4
+
5
+ ## Установка
6
+
7
+ ### npm
8
+
9
+ ```bash
10
+ npm install @notix/sdk
11
+ ```
12
+
13
+ ```js
14
+ import { Notix } from '@notix-hub/sdk';
15
+
16
+ const notix = new Notix({ token: 'ntx_xxxxxxxxxx' });
17
+
18
+ await notix.notify({
19
+ title: 'Новый заказ',
20
+ body: 'Клиент оформил заказ на 12 000 ₽',
21
+ type: 'order',
22
+ priority: 'high',
23
+ });
24
+ ```
25
+
26
+ ### CDN
27
+
28
+ ```html
29
+ <script src="https://notix-hub.ru/js/notify.js" data-token="ntx_..."></script>
30
+ ```
31
+
32
+ Скрипт автоматически инициализирует `window.notix` и включает захват форм.
33
+
34
+ Без своего домена — через jsdelivr:
35
+
36
+ ```html
37
+ <script src="https://cdn.jsdelivr.net/npm/@notix/sdk/dist/notify.min.js" data-token="ntx_..."></script>
38
+ ```
39
+
40
+ ## API
41
+
42
+ ### `new Notix(config)`
43
+
44
+ | Параметр | Тип | По умолчанию | Описание |
45
+ |----------|-----|-------------|----------|
46
+ | `token` | `string` | — | API-токен (префикс `ntx_`) |
47
+ | `endpoint` | `string` | `https://notix-hub.ru/api/v1/webhook` | URL вебхука |
48
+ | `autoCapture` | `boolean` | `true` | Автозахват форм при создании |
49
+ | `timeout` | `number` | `10000` | Таймаут запроса (мс) |
50
+ | `debug` | `boolean` | `false` | Логирование в консоль |
51
+ | `metrika` | `MetrikaConfig` | `{ enabled: auto }` | Настройки Яндекс.Метрики |
52
+
53
+ ### `notix.notify(payload)`
54
+
55
+ Отправляет уведомление. Возвращает `Promise<{ message, id }>`.
56
+
57
+ | Поле | Тип | Обязательно | Описание |
58
+ |------|-----|------------|----------|
59
+ | `title` | `string` | Да | Заголовок |
60
+ | `body` | `string` | Нет | Текст |
61
+ | `type` | `string` | Нет | Slug типа уведомления (lead, order, message...) |
62
+ | `priority` | `string` | Нет | `low`, `normal`, `high`, `urgent` |
63
+ | `payload` | `object` | Нет | Произвольный JSON |
64
+
65
+ ### `notix.capture(root?)`
66
+
67
+ Вручную активирует захват форм. `root` — элемент, в котором искать формы (по умолчанию `document`).
68
+
69
+ ### `notix.destroy()`
70
+
71
+ Отключает захват форм.
72
+
73
+ ## Автозахват форм
74
+
75
+ Добавьте `data-notify` к форме, и SDK перехватит `submit`:
76
+
77
+ ```html
78
+ <form data-notify data-notify-title="Заявка с лендинга">
79
+ <input name="name" data-notify-field="body">
80
+ <input name="phone">
81
+ <button type="submit">Отправить</button>
82
+ </form>
83
+ ```
84
+
85
+ Атрибуты:
86
+
87
+ | Атрибут | Описание |
88
+ |---------|----------|
89
+ | `data-notify` | Включает захват формы |
90
+ | `data-notify-title` | Заголовок уведомления (по умолчанию `document.title`) |
91
+ | `data-notify-body` | Статичный текст уведомления |
92
+ | `data-notify-type` | Тип уведомления |
93
+ | `data-notify-priority` | Приоритет |
94
+ | `data-notify-fields` | Имена полей через запятую (какие включить) |
95
+ | `data-notify-field` | Пометить поле (собирается как `name: value`) |
96
+
97
+ ## Яндекс.Метрика
98
+
99
+ SDK автоматически определяет наличие `window.ym` и отправляет события:
100
+
101
+ - `notix_sent` — уведомление отправлено
102
+ - `notix_error` — ошибка отправки
103
+ - `notix_form_captured` — захвачена форма
104
+
105
+ Отключить:
106
+
107
+ ```js
108
+ new Notix({ token: '...', metrika: { enabled: false } });
109
+ ```
110
+
111
+ ## Разработка
112
+
113
+ ```bash
114
+ npm install
115
+ npm run build # сборка в dist/
116
+ npm run typecheck # проверка типов
117
+ ```
118
+
119
+ ## Публикация
120
+
121
+ 1. Создать `.env` с токеном npm:
122
+ ```
123
+ NPM_TOKEN=npm_xxxxxxxxxxxxxxxxxxxx
124
+ ```
125
+
126
+ 2. Опубликовать:
127
+ ```bash
128
+ source .env && npm publish --access public
129
+ ```
130
+
131
+ После публикации пакет доступен:
132
+ - **npm**: `npm install @notix-hub/sdk`
133
+ - **jsdelivr (CDN)**: `https://cdn.jsdelivr.net/npm/@notix-hub/sdk/dist/notify.min.js`
134
+
135
+ ## Лицензия
136
+
137
+ MIT
@@ -0,0 +1,9 @@
1
+ import type { NotifyPayload } from './types';
2
+ interface FormMeta {
3
+ title?: string;
4
+ fieldsCount: number;
5
+ }
6
+ type OnSubmit = (payload: NotifyPayload, meta: FormMeta) => void;
7
+ export declare function initFormCapture(onSubmit: OnSubmit, root: Element | Document, debug: boolean): void;
8
+ export declare function destroyFormCapture(): void;
9
+ export {};
package/dist/http.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import type { NotifyPayload, NotixResponse } from './types';
2
+ export declare function sendWebhook(endpoint: string, token: string, payload: NotifyPayload, timeout?: number): Promise<NotixResponse>;
package/dist/index.cjs ADDED
@@ -0,0 +1,286 @@
1
+ /**
2
+ * @notix-hub/sdk — Notix (Нотикс) JavaScript SDK
3
+ * @version <%= pkg.version %>
4
+ * @license MIT
5
+ */
6
+ 'use strict';
7
+
8
+ const DEFAULT_TIMEOUT = 10000;
9
+ function sendWebhook(endpoint, token, payload, timeout = DEFAULT_TIMEOUT) {
10
+ const body = {
11
+ title: payload.title,
12
+ ...(payload.body !== undefined && { body: payload.body }),
13
+ ...(payload.type !== undefined && { notification_type: payload.type }),
14
+ ...(payload.priority !== undefined && { priority: payload.priority }),
15
+ };
16
+ const controller = new AbortController();
17
+ const timer = setTimeout(() => controller.abort(), timeout);
18
+ if (typeof fetch !== 'undefined') {
19
+ return fetch(endpoint, {
20
+ method: 'POST',
21
+ headers: {
22
+ 'Authorization': `Bearer ${token}`,
23
+ 'Content-Type': 'application/json',
24
+ 'Accept': 'application/json',
25
+ },
26
+ body: JSON.stringify(body),
27
+ signal: controller.signal,
28
+ })
29
+ .then(async (res) => {
30
+ clearTimeout(timer);
31
+ const json = await res.json();
32
+ if (!res.ok) {
33
+ throw new Error(json.error || json.message || `HTTP ${res.status}`);
34
+ }
35
+ return json;
36
+ })
37
+ .catch((err) => {
38
+ clearTimeout(timer);
39
+ throw err;
40
+ });
41
+ }
42
+ return xhrSend(endpoint, token, body, timeout);
43
+ }
44
+ function xhrSend(endpoint, token, body, timeout) {
45
+ return new Promise((resolve, reject) => {
46
+ const xhr = new XMLHttpRequest();
47
+ xhr.open('POST', endpoint, true);
48
+ xhr.setRequestHeader('Authorization', `Bearer ${token}`);
49
+ xhr.setRequestHeader('Content-Type', 'application/json');
50
+ xhr.setRequestHeader('Accept', 'application/json');
51
+ xhr.timeout = timeout;
52
+ xhr.onload = () => {
53
+ try {
54
+ const json = JSON.parse(xhr.responseText);
55
+ if (xhr.status >= 200 && xhr.status < 300) {
56
+ resolve(json);
57
+ }
58
+ else {
59
+ reject(new Error(json.error || json.message || `HTTP ${xhr.status}`));
60
+ }
61
+ }
62
+ catch {
63
+ reject(new Error('Invalid response'));
64
+ }
65
+ };
66
+ xhr.onerror = () => reject(new Error('Network error'));
67
+ xhr.ontimeout = () => reject(new Error('Request timeout'));
68
+ xhr.send(JSON.stringify(body));
69
+ });
70
+ }
71
+
72
+ const SUBMIT_DEBOUNCE_MS = 500;
73
+ const pendingForms = new WeakMap();
74
+ function initFormCapture(onSubmit, root, debug) {
75
+ const forms = root.querySelectorAll('form[data-notify]');
76
+ forms.forEach((form) => {
77
+ if (form.dataset.notixBound)
78
+ return;
79
+ form.dataset.notixBound = '1';
80
+ form.addEventListener('submit', (e) => {
81
+ const now = Date.now();
82
+ const last = pendingForms.get(form) ?? 0;
83
+ if (now - last < SUBMIT_DEBOUNCE_MS) {
84
+ e.preventDefault();
85
+ return;
86
+ }
87
+ pendingForms.set(form, now);
88
+ try {
89
+ const payload = extractPayload(form);
90
+ if (debug) {
91
+ console.log('[Notix] Form captured:', payload.title);
92
+ }
93
+ onSubmit(payload, { title: payload.title, fieldsCount: Object.keys(form.elements).length });
94
+ }
95
+ catch (err) {
96
+ // don't block form submission
97
+ if (debug) {
98
+ console.warn('[Notix] Form capture failed:', err);
99
+ }
100
+ }
101
+ });
102
+ });
103
+ }
104
+ function destroyFormCapture() {
105
+ document.querySelectorAll('form[data-notix-bound]').forEach((form) => {
106
+ delete form.dataset.notixBound;
107
+ // listeners are removed by page navigation; for SPA use Notix.destroy() before unmount
108
+ });
109
+ }
110
+ function extractPayload(form) {
111
+ const title = form.dataset.notifyTitle ?? form.dataset.notify_title ?? document.title;
112
+ const type = form.dataset.notifyType ?? form.dataset.notify_type;
113
+ const priority = form.dataset.notifyPriority ?? form.dataset.notify_priority;
114
+ const staticBody = form.dataset.notifyBody ?? form.dataset.notify_body;
115
+ const specifiedFields = (form.dataset.notifyFields ?? form.dataset.notify_fields)
116
+ ?.split(',')
117
+ .map((s) => s.trim())
118
+ .filter(Boolean);
119
+ if (staticBody) {
120
+ return {
121
+ title,
122
+ body: staticBody,
123
+ ...(type && { type }),
124
+ ...(priority && { priority: priority }),
125
+ };
126
+ }
127
+ const fieldEntries = [];
128
+ const elements = form.querySelectorAll('input[name], textarea[name], select[name]');
129
+ elements.forEach((el) => {
130
+ if (!el.name)
131
+ return;
132
+ if (specifiedFields && !specifiedFields.includes(el.name))
133
+ return;
134
+ const isMarked = el.hasAttribute('data-notify-field');
135
+ if (specifiedFields || isMarked) {
136
+ fieldEntries.push(`${el.name}: ${el.value || ''}`);
137
+ }
138
+ });
139
+ const body = fieldEntries.length > 0 ? fieldEntries.join('\n') : undefined;
140
+ return {
141
+ title,
142
+ ...(body && { body }),
143
+ ...(type && { type }),
144
+ ...(priority && { priority: priority }),
145
+ };
146
+ }
147
+
148
+ const DEFAULT_PREFIX = 'notix';
149
+ function createMetrikaTracker(config) {
150
+ if (config?.enabled === false)
151
+ return null;
152
+ const hasYm = typeof window !== 'undefined' && typeof window.ym === 'function';
153
+ if (!hasYm && !config?.enabled)
154
+ return null;
155
+ let counterId = config?.counterId;
156
+ if (!counterId && hasYm) {
157
+ counterId = detectCounterId();
158
+ }
159
+ if (!counterId)
160
+ return null;
161
+ return new MetrikaTracker(counterId, config?.prefix ?? DEFAULT_PREFIX);
162
+ }
163
+ function detectCounterId() {
164
+ for (const key of Object.keys(window)) {
165
+ if (key.startsWith('yaCounter')) {
166
+ const num = parseInt(key.replace('yaCounter', ''), 10);
167
+ if (!isNaN(num))
168
+ return num;
169
+ }
170
+ }
171
+ return undefined;
172
+ }
173
+ class MetrikaTracker {
174
+ constructor(counterId, prefix) {
175
+ this.counterId = counterId;
176
+ this.prefix = prefix;
177
+ }
178
+ trackSent(response) {
179
+ this.reachGoal(`${this.prefix}_sent`, {
180
+ id: response.id,
181
+ message: response.message,
182
+ });
183
+ }
184
+ trackError(error) {
185
+ this.reachGoal(`${this.prefix}_error`, {
186
+ error: error.message,
187
+ });
188
+ }
189
+ trackFormCaptured(title, fieldsCount) {
190
+ this.reachGoal(`${this.prefix}_form_captured`, {
191
+ title,
192
+ fields: fieldsCount,
193
+ });
194
+ }
195
+ trackGoal(name) {
196
+ this.reachGoal(name, {});
197
+ }
198
+ reachGoal(target, params) {
199
+ if (typeof window.ym === 'function') {
200
+ window.ym(this.counterId, 'reachGoal', target, params);
201
+ }
202
+ }
203
+ }
204
+
205
+ const DEFAULT_ENDPOINT = 'https://notix-hub.ru/api/v1/webhook';
206
+ class Notix {
207
+ constructor(config) {
208
+ this.captureActive = false;
209
+ if (!config.token || !config.token.startsWith('ntx_')) {
210
+ throw new Error('Notix: token must start with "ntx_"');
211
+ }
212
+ this.token = config.token;
213
+ this.endpoint = config.endpoint ?? DEFAULT_ENDPOINT;
214
+ this.timeout = config.timeout ?? 10000;
215
+ this.debug = config.debug ?? false;
216
+ this.metrika = createMetrikaTracker(config.metrika);
217
+ if (config.autoCapture !== false) {
218
+ this.capture();
219
+ }
220
+ }
221
+ async notify(payload) {
222
+ if (!payload.title) {
223
+ throw new Error('Notix: title is required');
224
+ }
225
+ this.log('Sending notification:', payload.title);
226
+ try {
227
+ const response = await sendWebhook(this.endpoint, this.token, payload, this.timeout);
228
+ this.log('Notification sent:', response.id);
229
+ this.metrika?.trackSent(response);
230
+ return response;
231
+ }
232
+ catch (error) {
233
+ const err = error instanceof Error ? error : new Error(String(error));
234
+ this.log('Error sending notification:', err.message);
235
+ this.metrika?.trackError(err);
236
+ throw err;
237
+ }
238
+ }
239
+ capture(root = document) {
240
+ if (this.captureActive)
241
+ return;
242
+ initFormCapture((payload, meta) => {
243
+ this.notify(payload).then(() => {
244
+ this.metrika?.trackFormCaptured(meta.title ?? payload.title, meta.fieldsCount);
245
+ }).catch(() => { });
246
+ }, root, this.debug);
247
+ this.captureActive = true;
248
+ this.log('Form capture activated');
249
+ }
250
+ destroy() {
251
+ destroyFormCapture();
252
+ this.captureActive = false;
253
+ this.log('Notix destroyed');
254
+ }
255
+ log(...args) {
256
+ if (this.debug) {
257
+ console.log('[Notix]', ...args);
258
+ }
259
+ }
260
+ }
261
+
262
+ if (typeof document !== 'undefined') {
263
+ const thisScript = document.currentScript;
264
+ if (thisScript) {
265
+ const token = thisScript.dataset.token;
266
+ if (token) {
267
+ const endpoint = thisScript.dataset.endpoint;
268
+ const autoCapture = thisScript.dataset.autoCapture !== 'false';
269
+ const debug = thisScript.dataset.debug === 'true';
270
+ const timeout = thisScript.dataset.timeout ? parseInt(thisScript.dataset.timeout, 10) : undefined;
271
+ const notix = new Notix({
272
+ token,
273
+ ...(endpoint && { endpoint }),
274
+ autoCapture,
275
+ debug,
276
+ ...(timeout && { timeout }),
277
+ });
278
+ window.notix = notix;
279
+ }
280
+ else if (thisScript.dataset.debug === 'true') {
281
+ console.warn('[Notix] data-token attribute is required on script tag');
282
+ }
283
+ }
284
+ }
285
+
286
+ exports.Notix = Notix;
@@ -0,0 +1,3 @@
1
+ import { Notix } from './notix';
2
+ export { Notix };
3
+ export type { NotixConfig, NotifyPayload, NotixResponse, MetrikaConfig } from './types';
package/dist/index.mjs ADDED
@@ -0,0 +1,284 @@
1
+ /**
2
+ * @notix-hub/sdk — Notix (Нотикс) JavaScript SDK
3
+ * @version <%= pkg.version %>
4
+ * @license MIT
5
+ */
6
+ const DEFAULT_TIMEOUT = 10000;
7
+ function sendWebhook(endpoint, token, payload, timeout = DEFAULT_TIMEOUT) {
8
+ const body = {
9
+ title: payload.title,
10
+ ...(payload.body !== undefined && { body: payload.body }),
11
+ ...(payload.type !== undefined && { notification_type: payload.type }),
12
+ ...(payload.priority !== undefined && { priority: payload.priority }),
13
+ };
14
+ const controller = new AbortController();
15
+ const timer = setTimeout(() => controller.abort(), timeout);
16
+ if (typeof fetch !== 'undefined') {
17
+ return fetch(endpoint, {
18
+ method: 'POST',
19
+ headers: {
20
+ 'Authorization': `Bearer ${token}`,
21
+ 'Content-Type': 'application/json',
22
+ 'Accept': 'application/json',
23
+ },
24
+ body: JSON.stringify(body),
25
+ signal: controller.signal,
26
+ })
27
+ .then(async (res) => {
28
+ clearTimeout(timer);
29
+ const json = await res.json();
30
+ if (!res.ok) {
31
+ throw new Error(json.error || json.message || `HTTP ${res.status}`);
32
+ }
33
+ return json;
34
+ })
35
+ .catch((err) => {
36
+ clearTimeout(timer);
37
+ throw err;
38
+ });
39
+ }
40
+ return xhrSend(endpoint, token, body, timeout);
41
+ }
42
+ function xhrSend(endpoint, token, body, timeout) {
43
+ return new Promise((resolve, reject) => {
44
+ const xhr = new XMLHttpRequest();
45
+ xhr.open('POST', endpoint, true);
46
+ xhr.setRequestHeader('Authorization', `Bearer ${token}`);
47
+ xhr.setRequestHeader('Content-Type', 'application/json');
48
+ xhr.setRequestHeader('Accept', 'application/json');
49
+ xhr.timeout = timeout;
50
+ xhr.onload = () => {
51
+ try {
52
+ const json = JSON.parse(xhr.responseText);
53
+ if (xhr.status >= 200 && xhr.status < 300) {
54
+ resolve(json);
55
+ }
56
+ else {
57
+ reject(new Error(json.error || json.message || `HTTP ${xhr.status}`));
58
+ }
59
+ }
60
+ catch {
61
+ reject(new Error('Invalid response'));
62
+ }
63
+ };
64
+ xhr.onerror = () => reject(new Error('Network error'));
65
+ xhr.ontimeout = () => reject(new Error('Request timeout'));
66
+ xhr.send(JSON.stringify(body));
67
+ });
68
+ }
69
+
70
+ const SUBMIT_DEBOUNCE_MS = 500;
71
+ const pendingForms = new WeakMap();
72
+ function initFormCapture(onSubmit, root, debug) {
73
+ const forms = root.querySelectorAll('form[data-notify]');
74
+ forms.forEach((form) => {
75
+ if (form.dataset.notixBound)
76
+ return;
77
+ form.dataset.notixBound = '1';
78
+ form.addEventListener('submit', (e) => {
79
+ const now = Date.now();
80
+ const last = pendingForms.get(form) ?? 0;
81
+ if (now - last < SUBMIT_DEBOUNCE_MS) {
82
+ e.preventDefault();
83
+ return;
84
+ }
85
+ pendingForms.set(form, now);
86
+ try {
87
+ const payload = extractPayload(form);
88
+ if (debug) {
89
+ console.log('[Notix] Form captured:', payload.title);
90
+ }
91
+ onSubmit(payload, { title: payload.title, fieldsCount: Object.keys(form.elements).length });
92
+ }
93
+ catch (err) {
94
+ // don't block form submission
95
+ if (debug) {
96
+ console.warn('[Notix] Form capture failed:', err);
97
+ }
98
+ }
99
+ });
100
+ });
101
+ }
102
+ function destroyFormCapture() {
103
+ document.querySelectorAll('form[data-notix-bound]').forEach((form) => {
104
+ delete form.dataset.notixBound;
105
+ // listeners are removed by page navigation; for SPA use Notix.destroy() before unmount
106
+ });
107
+ }
108
+ function extractPayload(form) {
109
+ const title = form.dataset.notifyTitle ?? form.dataset.notify_title ?? document.title;
110
+ const type = form.dataset.notifyType ?? form.dataset.notify_type;
111
+ const priority = form.dataset.notifyPriority ?? form.dataset.notify_priority;
112
+ const staticBody = form.dataset.notifyBody ?? form.dataset.notify_body;
113
+ const specifiedFields = (form.dataset.notifyFields ?? form.dataset.notify_fields)
114
+ ?.split(',')
115
+ .map((s) => s.trim())
116
+ .filter(Boolean);
117
+ if (staticBody) {
118
+ return {
119
+ title,
120
+ body: staticBody,
121
+ ...(type && { type }),
122
+ ...(priority && { priority: priority }),
123
+ };
124
+ }
125
+ const fieldEntries = [];
126
+ const elements = form.querySelectorAll('input[name], textarea[name], select[name]');
127
+ elements.forEach((el) => {
128
+ if (!el.name)
129
+ return;
130
+ if (specifiedFields && !specifiedFields.includes(el.name))
131
+ return;
132
+ const isMarked = el.hasAttribute('data-notify-field');
133
+ if (specifiedFields || isMarked) {
134
+ fieldEntries.push(`${el.name}: ${el.value || ''}`);
135
+ }
136
+ });
137
+ const body = fieldEntries.length > 0 ? fieldEntries.join('\n') : undefined;
138
+ return {
139
+ title,
140
+ ...(body && { body }),
141
+ ...(type && { type }),
142
+ ...(priority && { priority: priority }),
143
+ };
144
+ }
145
+
146
+ const DEFAULT_PREFIX = 'notix';
147
+ function createMetrikaTracker(config) {
148
+ if (config?.enabled === false)
149
+ return null;
150
+ const hasYm = typeof window !== 'undefined' && typeof window.ym === 'function';
151
+ if (!hasYm && !config?.enabled)
152
+ return null;
153
+ let counterId = config?.counterId;
154
+ if (!counterId && hasYm) {
155
+ counterId = detectCounterId();
156
+ }
157
+ if (!counterId)
158
+ return null;
159
+ return new MetrikaTracker(counterId, config?.prefix ?? DEFAULT_PREFIX);
160
+ }
161
+ function detectCounterId() {
162
+ for (const key of Object.keys(window)) {
163
+ if (key.startsWith('yaCounter')) {
164
+ const num = parseInt(key.replace('yaCounter', ''), 10);
165
+ if (!isNaN(num))
166
+ return num;
167
+ }
168
+ }
169
+ return undefined;
170
+ }
171
+ class MetrikaTracker {
172
+ constructor(counterId, prefix) {
173
+ this.counterId = counterId;
174
+ this.prefix = prefix;
175
+ }
176
+ trackSent(response) {
177
+ this.reachGoal(`${this.prefix}_sent`, {
178
+ id: response.id,
179
+ message: response.message,
180
+ });
181
+ }
182
+ trackError(error) {
183
+ this.reachGoal(`${this.prefix}_error`, {
184
+ error: error.message,
185
+ });
186
+ }
187
+ trackFormCaptured(title, fieldsCount) {
188
+ this.reachGoal(`${this.prefix}_form_captured`, {
189
+ title,
190
+ fields: fieldsCount,
191
+ });
192
+ }
193
+ trackGoal(name) {
194
+ this.reachGoal(name, {});
195
+ }
196
+ reachGoal(target, params) {
197
+ if (typeof window.ym === 'function') {
198
+ window.ym(this.counterId, 'reachGoal', target, params);
199
+ }
200
+ }
201
+ }
202
+
203
+ const DEFAULT_ENDPOINT = 'https://notix-hub.ru/api/v1/webhook';
204
+ class Notix {
205
+ constructor(config) {
206
+ this.captureActive = false;
207
+ if (!config.token || !config.token.startsWith('ntx_')) {
208
+ throw new Error('Notix: token must start with "ntx_"');
209
+ }
210
+ this.token = config.token;
211
+ this.endpoint = config.endpoint ?? DEFAULT_ENDPOINT;
212
+ this.timeout = config.timeout ?? 10000;
213
+ this.debug = config.debug ?? false;
214
+ this.metrika = createMetrikaTracker(config.metrika);
215
+ if (config.autoCapture !== false) {
216
+ this.capture();
217
+ }
218
+ }
219
+ async notify(payload) {
220
+ if (!payload.title) {
221
+ throw new Error('Notix: title is required');
222
+ }
223
+ this.log('Sending notification:', payload.title);
224
+ try {
225
+ const response = await sendWebhook(this.endpoint, this.token, payload, this.timeout);
226
+ this.log('Notification sent:', response.id);
227
+ this.metrika?.trackSent(response);
228
+ return response;
229
+ }
230
+ catch (error) {
231
+ const err = error instanceof Error ? error : new Error(String(error));
232
+ this.log('Error sending notification:', err.message);
233
+ this.metrika?.trackError(err);
234
+ throw err;
235
+ }
236
+ }
237
+ capture(root = document) {
238
+ if (this.captureActive)
239
+ return;
240
+ initFormCapture((payload, meta) => {
241
+ this.notify(payload).then(() => {
242
+ this.metrika?.trackFormCaptured(meta.title ?? payload.title, meta.fieldsCount);
243
+ }).catch(() => { });
244
+ }, root, this.debug);
245
+ this.captureActive = true;
246
+ this.log('Form capture activated');
247
+ }
248
+ destroy() {
249
+ destroyFormCapture();
250
+ this.captureActive = false;
251
+ this.log('Notix destroyed');
252
+ }
253
+ log(...args) {
254
+ if (this.debug) {
255
+ console.log('[Notix]', ...args);
256
+ }
257
+ }
258
+ }
259
+
260
+ if (typeof document !== 'undefined') {
261
+ const thisScript = document.currentScript;
262
+ if (thisScript) {
263
+ const token = thisScript.dataset.token;
264
+ if (token) {
265
+ const endpoint = thisScript.dataset.endpoint;
266
+ const autoCapture = thisScript.dataset.autoCapture !== 'false';
267
+ const debug = thisScript.dataset.debug === 'true';
268
+ const timeout = thisScript.dataset.timeout ? parseInt(thisScript.dataset.timeout, 10) : undefined;
269
+ const notix = new Notix({
270
+ token,
271
+ ...(endpoint && { endpoint }),
272
+ autoCapture,
273
+ debug,
274
+ ...(timeout && { timeout }),
275
+ });
276
+ window.notix = notix;
277
+ }
278
+ else if (thisScript.dataset.debug === 'true') {
279
+ console.warn('[Notix] data-token attribute is required on script tag');
280
+ }
281
+ }
282
+ }
283
+
284
+ export { Notix };
@@ -0,0 +1,24 @@
1
+ import type { MetrikaConfig, NotixResponse } from './types';
2
+ declare global {
3
+ interface Window {
4
+ ym?: (counterId: number, event: string, target: string, params?: Record<string, unknown>) => void;
5
+ Ya?: {
6
+ Metrika?: {
7
+ counterById?: Map<number, {
8
+ hit: (url: string, params?: Record<string, unknown>) => void;
9
+ }>;
10
+ };
11
+ };
12
+ }
13
+ }
14
+ export declare function createMetrikaTracker(config?: MetrikaConfig): MetrikaTracker | null;
15
+ export declare class MetrikaTracker {
16
+ private counterId;
17
+ private prefix;
18
+ constructor(counterId: number, prefix: string);
19
+ trackSent(response: NotixResponse): void;
20
+ trackError(error: Error): void;
21
+ trackFormCaptured(title: string, fieldsCount: number): void;
22
+ trackGoal(name: string): void;
23
+ private reachGoal;
24
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @notix-hub/sdk — Notix (Нотикс) JavaScript SDK
3
+ * @version <%= pkg.version %>
4
+ * @license MIT
5
+ */
6
+ var Notix=function(t){"use strict";function e(t,e,o,i=1e4){const r={title:o.title,...void 0!==o.body&&{body:o.body},...void 0!==o.type&&{notification_type:o.type},...void 0!==o.priority&&{priority:o.priority}},n=new AbortController,a=setTimeout(()=>n.abort(),i);return"undefined"!=typeof fetch?fetch(t,{method:"POST",headers:{Authorization:`Bearer ${e}`,"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(r),signal:n.signal}).then(async t=>{clearTimeout(a);const e=await t.json();if(!t.ok)throw new Error(e.error||e.message||`HTTP ${t.status}`);return e}).catch(t=>{throw clearTimeout(a),t}):function(t,e,o,i){return new Promise((r,n)=>{const a=new XMLHttpRequest;a.open("POST",t,!0),a.setRequestHeader("Authorization",`Bearer ${e}`),a.setRequestHeader("Content-Type","application/json"),a.setRequestHeader("Accept","application/json"),a.timeout=i,a.onload=()=>{try{const t=JSON.parse(a.responseText);a.status>=200&&a.status<300?r(t):n(new Error(t.error||t.message||`HTTP ${a.status}`))}catch{n(new Error("Invalid response"))}},a.onerror=()=>n(new Error("Network error")),a.ontimeout=()=>n(new Error("Request timeout")),a.send(JSON.stringify(o))})}(t,e,r,i)}const o=new WeakMap;function i(t,e,i){e.querySelectorAll("form[data-notify]").forEach(e=>{e.dataset.notixBound||(e.dataset.notixBound="1",e.addEventListener("submit",r=>{const n=Date.now();if(n-(o.get(e)??0)<500)r.preventDefault();else{o.set(e,n);try{const o=function(t){const e=t.dataset.notifyTitle??t.dataset.notify_title??document.title,o=t.dataset.notifyType??t.dataset.notify_type,i=t.dataset.notifyPriority??t.dataset.notify_priority,r=t.dataset.notifyBody??t.dataset.notify_body,n=(t.dataset.notifyFields??t.dataset.notify_fields)?.split(",").map(t=>t.trim()).filter(Boolean);if(r)return{title:e,body:r,...o&&{type:o},...i&&{priority:i}};const a=[],s=t.querySelectorAll("input[name], textarea[name], select[name]");s.forEach(t=>{if(!t.name)return;if(n&&!n.includes(t.name))return;const e=t.hasAttribute("data-notify-field");(n||e)&&a.push(`${t.name}: ${t.value||""}`)});const c=a.length>0?a.join("\n"):void 0;return{title:e,...c&&{body:c},...o&&{type:o},...i&&{priority:i}}}(e);i&&console.log("[Notix] Form captured:",o.title),t(o,{title:o.title,fieldsCount:Object.keys(e.elements).length})}catch(t){i&&console.warn("[Notix] Form capture failed:",t)}}}))})}function r(t){if(!1===t?.enabled)return null;const e="undefined"!=typeof window&&"function"==typeof window.ym;if(!e&&!t?.enabled)return null;let o=t?.counterId;return!o&&e&&(o=function(){for(const t of Object.keys(window))if(t.startsWith("yaCounter")){const e=parseInt(t.replace("yaCounter",""),10);if(!isNaN(e))return e}return}()),o?new n(o,t?.prefix??"notix"):null}class n{constructor(t,e){this.counterId=t,this.prefix=e}trackSent(t){this.reachGoal(`${this.prefix}_sent`,{id:t.id,message:t.message})}trackError(t){this.reachGoal(`${this.prefix}_error`,{error:t.message})}trackFormCaptured(t,e){this.reachGoal(`${this.prefix}_form_captured`,{title:t,fields:e})}trackGoal(t){this.reachGoal(t,{})}reachGoal(t,e){"function"==typeof window.ym&&window.ym(this.counterId,"reachGoal",t,e)}}class a{constructor(t){if(this.captureActive=!1,!t.token||!t.token.startsWith("ntx_"))throw new Error('Notix: token must start with "ntx_"');this.token=t.token,this.endpoint=t.endpoint??"https://notix-hub.ru/api/v1/webhook",this.timeout=t.timeout??1e4,this.debug=t.debug??!1,this.metrika=r(t.metrika),!1!==t.autoCapture&&this.capture()}async notify(t){if(!t.title)throw new Error("Notix: title is required");this.log("Sending notification:",t.title);try{const o=await e(this.endpoint,this.token,t,this.timeout);return this.log("Notification sent:",o.id),this.metrika?.trackSent(o),o}catch(t){const e=t instanceof Error?t:new Error(String(t));throw this.log("Error sending notification:",e.message),this.metrika?.trackError(e),e}}capture(t=document){this.captureActive||(i((t,e)=>{this.notify(t).then(()=>{this.metrika?.trackFormCaptured(e.title??t.title,e.fieldsCount)}).catch(()=>{})},t,this.debug),this.captureActive=!0,this.log("Form capture activated"))}destroy(){document.querySelectorAll("form[data-notix-bound]").forEach(t=>{delete t.dataset.notixBound}),this.captureActive=!1,this.log("Notix destroyed")}log(...t){this.debug&&console.log("[Notix]",...t)}}if("undefined"!=typeof document){const t=document.currentScript;if(t){const e=t.dataset.token;if(e){const o=t.dataset.endpoint,i="false"!==t.dataset.autoCapture,r="true"===t.dataset.debug,n=t.dataset.timeout?parseInt(t.dataset.timeout,10):void 0,s=new a({token:e,...o&&{endpoint:o},autoCapture:i,debug:r,...n&&{timeout:n}});window.notix=s}else"true"===t.dataset.debug&&console.warn("[Notix] data-token attribute is required on script tag")}}return t.Notix=a,t}({});
@@ -0,0 +1,17 @@
1
+ import type { NotixConfig, NotifyPayload } from './types';
2
+ export declare class Notix {
3
+ private token;
4
+ private endpoint;
5
+ private timeout;
6
+ private debug;
7
+ private metrika;
8
+ private captureActive;
9
+ constructor(config: NotixConfig);
10
+ notify(payload: NotifyPayload): Promise<{
11
+ message: string;
12
+ id: string;
13
+ }>;
14
+ capture(root?: Element | Document): void;
15
+ destroy(): void;
16
+ private log;
17
+ }
@@ -0,0 +1,28 @@
1
+ export type NotificationPriority = 'low' | 'normal' | 'high' | 'urgent';
2
+ export interface NotifyPayload {
3
+ title: string;
4
+ body?: string;
5
+ type?: string;
6
+ priority?: NotificationPriority;
7
+ payload?: Record<string, unknown>;
8
+ }
9
+ export interface NotifyRequestBody extends NotifyPayload {
10
+ notification_type?: string;
11
+ }
12
+ export interface NotixResponse {
13
+ message: string;
14
+ id: string;
15
+ }
16
+ export interface MetrikaConfig {
17
+ enabled?: boolean;
18
+ counterId?: number;
19
+ prefix?: string;
20
+ }
21
+ export interface NotixConfig {
22
+ token: string;
23
+ endpoint?: string;
24
+ autoCapture?: boolean;
25
+ metrika?: MetrikaConfig;
26
+ timeout?: number;
27
+ debug?: boolean;
28
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@notix-hub/sdk",
3
+ "version": "0.1.0",
4
+ "description": "JavaScript SDK for Notix (Нотикс) — notification aggregation service",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.cjs",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "rollup -c",
21
+ "dev": "rollup -c -w",
22
+ "typecheck": "tsc --noEmit",
23
+ "prepare": "npm run build",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "keywords": [
27
+ "notix",
28
+ "notifications",
29
+ "webhook",
30
+ "notify"
31
+ ],
32
+ "license": "MIT",
33
+ "devDependencies": {
34
+ "@rollup/plugin-typescript": "^12.0.0",
35
+ "@rollup/plugin-terser": "^0.4.0",
36
+ "rollup": "^4.0.0",
37
+ "tslib": "^2.0.0",
38
+ "typescript": "^5.0.0"
39
+ }
40
+ }