@notix-hub/sdk 0.3.1 → 0.3.3

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/dist/capture.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import type { NotifyPayload } from './types';
1
+ import type { NotifyPayload, NotixResponse } from './types';
2
2
  interface FormMeta {
3
3
  title?: string;
4
4
  fieldsCount: number;
5
5
  }
6
6
  type OnSubmit = (payload: NotifyPayload, meta: FormMeta) => void;
7
- export declare function initFormCapture(onSubmit: OnSubmit, root: Element | Document, debug: boolean): void;
7
+ export declare function initFormCapture(onSubmit: OnSubmit, root: Element | Document, debug: boolean, onCapture?: (response: NotixResponse, payload: NotifyPayload) => void): void;
8
8
  export declare function destroyFormCapture(): void;
9
9
  export {};
package/dist/errors.d.ts CHANGED
@@ -5,14 +5,21 @@ interface ErrorEntry {
5
5
  col: number;
6
6
  stack: string;
7
7
  ts: string;
8
+ source: string;
9
+ }
10
+ interface ErrorBatch {
11
+ page: string;
12
+ errors: ErrorEntry[];
8
13
  }
9
14
  export declare class ErrorBatcher {
10
15
  private sendFn;
11
- private errors;
12
- private timer;
13
- constructor(sendFn: (errors: ErrorEntry[]) => void);
16
+ private currentPage;
17
+ constructor(sendFn: (batches: ErrorBatch[]) => void);
14
18
  private bindGlobal;
15
- private startTimer;
19
+ private addError;
20
+ private load;
21
+ private save;
16
22
  flush(): void;
23
+ clearAll(): void;
17
24
  }
18
25
  export {};
package/dist/index.cjs CHANGED
@@ -13,6 +13,7 @@ function sendWebhook(endpoint, token, payload, timeout = DEFAULT_TIMEOUT) {
13
13
  ...(payload.type !== undefined && { notification_type: payload.type }),
14
14
  ...(payload.priority !== undefined && { priority: payload.priority }),
15
15
  ...(payload.tag !== undefined && { tag: payload.tag }),
16
+ ...(payload.tags !== undefined && { tags: payload.tags }),
16
17
  ...(payload.payload !== undefined && { payload: payload.payload }),
17
18
  };
18
19
  const controller = new AbortController();
@@ -27,6 +28,7 @@ function sendWebhook(endpoint, token, payload, timeout = DEFAULT_TIMEOUT) {
27
28
  },
28
29
  body: JSON.stringify(body),
29
30
  signal: controller.signal,
31
+ keepalive: true,
30
32
  })
31
33
  .then(async (res) => {
32
34
  clearTimeout(timer);
@@ -73,17 +75,17 @@ function xhrSend(endpoint, token, body, timeout) {
73
75
 
74
76
  const SUBMIT_DEBOUNCE_MS = 500;
75
77
  const pendingForms = new WeakMap();
76
- function initFormCapture(onSubmit, root, debug) {
77
- const forms = root.querySelectorAll('form[data-notify]');
78
+ function initFormCapture(onSubmit, root, debug, onCapture) {
79
+ const forms = root.querySelectorAll('form[data-notix], form[data-notify]');
78
80
  forms.forEach((form) => {
79
81
  if (form.dataset.notixBound)
80
82
  return;
81
83
  form.dataset.notixBound = '1';
82
84
  form.addEventListener('submit', (e) => {
85
+ e.preventDefault();
83
86
  const now = Date.now();
84
87
  const last = pendingForms.get(form) ?? 0;
85
88
  if (now - last < SUBMIT_DEBOUNCE_MS) {
86
- e.preventDefault();
87
89
  return;
88
90
  }
89
91
  pendingForms.set(form, now);
@@ -92,10 +94,19 @@ function initFormCapture(onSubmit, root, debug) {
92
94
  if (debug) {
93
95
  console.log('[Notix] Form captured:', payload.title);
94
96
  }
95
- onSubmit(payload, { title: payload.title, fieldsCount: Object.keys(form.elements).length });
97
+ onSubmit(payload, { title: payload.title, fieldsCount: payload.payload ? Object.keys(payload.payload).length : 0 });
98
+ // Per-form callback: data-notix-onsuccess="funcName"
99
+ const callbackName = attr(form, 'onsuccess');
100
+ if (callbackName) {
101
+ const fn = window[callbackName];
102
+ if (typeof fn === 'function') {
103
+ // The actual callback fires after notify() resolves — handled in notix.ts
104
+ form.dataset.notixCallback = callbackName;
105
+ }
106
+ }
107
+ if (onCapture) ;
96
108
  }
97
109
  catch (err) {
98
- // don't block form submission
99
110
  if (debug) {
100
111
  console.warn('[Notix] Form capture failed:', err);
101
112
  }
@@ -106,64 +117,91 @@ function initFormCapture(onSubmit, root, debug) {
106
117
  function destroyFormCapture() {
107
118
  document.querySelectorAll('form[data-notix-bound]').forEach((form) => {
108
119
  delete form.dataset.notixBound;
109
- // listeners are removed by page navigation; for SPA use Notix.destroy() before unmount
110
120
  });
111
121
  }
122
+ function attr(form, name) {
123
+ const camel = name.charAt(0).toUpperCase() + name.slice(1);
124
+ return form.dataset[`notix${camel}`] ?? form.dataset[`notify${camel}`];
125
+ }
126
+ function elLabel(el) {
127
+ return el.getAttribute('data-notix-label') ?? el.getAttribute('data-notify-label') ?? el.name;
128
+ }
112
129
  function extractPayload(form) {
113
- const title = form.dataset.notifyTitle ?? form.dataset.notify_title ?? document.title;
114
- const type = form.dataset.notifyType ?? form.dataset.notify_type;
115
- const tag = form.dataset.notifyTag ?? form.dataset.notify_tag;
116
- const priority = form.dataset.notifyPriority ?? form.dataset.notify_priority;
117
- const staticBody = form.dataset.notifyBody ?? form.dataset.notify_body;
118
- const template = form.dataset.notifyTemplate ?? form.dataset.notify_template;
119
- const specifiedFields = (form.dataset.notifyFields ?? form.dataset.notify_fields)
120
- ?.split(',')
121
- .map((s) => s.trim())
122
- .filter(Boolean);
123
- if (staticBody) {
124
- return {
125
- title,
126
- body: staticBody,
127
- ...(type && { type }),
128
- ...(tag && { tag }),
129
- ...(priority && { priority: priority }),
130
- };
130
+ const title = attr(form, 'title') ?? document.title;
131
+ const typeSlug = attr(form, 'type');
132
+ const tag = attr(form, 'tag');
133
+ const tagsStr = attr(form, 'tags');
134
+ const priority = attr(form, 'priority');
135
+ const bodyTemplate = attr(form, 'body');
136
+ const fieldsStr = attr(form, 'fields');
137
+ const allowedNames = new Set();
138
+ if (fieldsStr) {
139
+ fieldsStr.split(',').map(s => s.trim()).filter(Boolean).forEach(name => allowedNames.add(name));
131
140
  }
141
+ if (bodyTemplate) {
142
+ const matches = bodyTemplate.matchAll(/\{(\w+)\}/g);
143
+ for (const m of matches) {
144
+ allowedNames.add(m[1]);
145
+ }
146
+ }
147
+ let tags;
148
+ if (tag) {
149
+ tags = [tag];
150
+ }
151
+ if (tagsStr) {
152
+ try {
153
+ const parsed = JSON.parse(tagsStr);
154
+ tags = [...(tags || []), ...(Array.isArray(parsed) ? parsed : [parsed])];
155
+ }
156
+ catch {
157
+ tags = tags || [];
158
+ tags.push(tagsStr);
159
+ }
160
+ }
161
+ const fieldValues = {};
132
162
  const elements = form.querySelectorAll('input[name], textarea[name], select[name]');
133
- const fieldMap = new Map();
134
163
  elements.forEach((el) => {
135
164
  if (!el.name)
136
165
  return;
166
+ if (!allowedNames.has(el.name))
167
+ return;
137
168
  if (el.type === 'checkbox' || el.type === 'radio') {
138
169
  if (!el.checked)
139
170
  return;
140
171
  }
141
- if (specifiedFields && !specifiedFields.includes(el.name))
142
- return;
143
- const isMarked = el.hasAttribute('data-notify-field');
144
- if (specifiedFields || isMarked || (!specifiedFields && !template)) {
145
- const label = el.getAttribute('data-notify-label') ?? el.name;
146
- fieldMap.set(label, el.value || '');
147
- }
172
+ fieldValues[el.name] = el.value || '';
148
173
  });
149
174
  let body;
150
- if (template) {
151
- body = template.replace(/\{(\w+)\}/g, (_, key) => {
152
- if (fieldMap.has(key))
153
- return fieldMap.get(key);
154
- const el = form.elements.namedItem(key);
155
- return el?.value || `{${key}}`;
156
- });
175
+ if (bodyTemplate) {
176
+ body = bodyTemplate.replace(/\{(\w+)\}/g, (_, fieldName) => fieldValues[fieldName] ?? `{${fieldName}}`);
157
177
  }
158
- else if (fieldMap.size > 0) {
159
- body = Array.from(fieldMap, ([k, v]) => `${k}: ${v}`).join('\n');
178
+ const payloadData = {};
179
+ elements.forEach((el) => {
180
+ if (!el.name)
181
+ return;
182
+ if (!allowedNames.has(el.name))
183
+ return;
184
+ if ((el.type === 'checkbox' || el.type === 'radio') && !el.checked)
185
+ return;
186
+ payloadData[elLabel(el)] = el.value || '';
187
+ });
188
+ let extraPayload = {};
189
+ const payloadJson = attr(form, 'payload');
190
+ if (payloadJson) {
191
+ try {
192
+ extraPayload = JSON.parse(payloadJson);
193
+ }
194
+ catch {
195
+ // ignore invalid JSON
196
+ }
160
197
  }
161
198
  return {
162
199
  title,
163
200
  ...(body && { body }),
164
- ...(type && { type }),
165
- ...(tag && { tag }),
201
+ ...(typeSlug && { type: typeSlug }),
202
+ ...(tags && tags.length > 0 && { tags }),
166
203
  ...(priority && { priority: priority }),
204
+ payload: { ...payloadData, ...extraPayload },
167
205
  };
168
206
  }
169
207
 
@@ -224,44 +262,39 @@ class MetrikaTracker {
224
262
  }
225
263
  }
226
264
 
227
- const BATCH_SIZE = 10;
228
- const FLUSH_INTERVAL = 60000;
265
+ const STORAGE_KEY = 'notix_errors';
266
+ const MAX_ERRORS = 500;
229
267
  class ErrorBatcher {
230
268
  constructor(sendFn) {
231
269
  this.sendFn = sendFn;
232
- this.errors = [];
233
- this.timer = null;
270
+ this.currentPage = location.pathname;
234
271
  this.bindGlobal();
235
272
  }
236
273
  bindGlobal() {
237
274
  if (typeof window === 'undefined')
238
275
  return;
239
- const collect = (ev) => {
240
- this.errors.push({
276
+ const collect = (source) => (ev) => {
277
+ this.addError({
241
278
  message: ev.message || 'Unknown error',
242
279
  file: ev.filename || '',
243
280
  line: ev.lineno || 0,
244
281
  col: ev.colno || 0,
245
282
  stack: ev.error?.stack || '',
246
283
  ts: new Date().toISOString(),
284
+ source,
247
285
  });
248
- if (this.errors.length >= BATCH_SIZE)
249
- this.flush();
250
- this.startTimer();
251
286
  };
252
- window.addEventListener('error', collect);
287
+ window.addEventListener('error', collect('onerror'));
253
288
  window.addEventListener('unhandledrejection', (ev) => {
254
- this.errors.push({
289
+ this.addError({
255
290
  message: ev.reason?.message || String(ev.reason),
256
291
  file: '',
257
292
  line: 0,
258
293
  col: 0,
259
294
  stack: ev.reason?.stack || '',
260
295
  ts: new Date().toISOString(),
296
+ source: 'unhandledrejection',
261
297
  });
262
- if (this.errors.length >= BATCH_SIZE)
263
- this.flush();
264
- this.startTimer();
265
298
  });
266
299
  window.addEventListener('beforeunload', () => this.flush());
267
300
  document.addEventListener('visibilitychange', () => {
@@ -269,19 +302,49 @@ class ErrorBatcher {
269
302
  this.flush();
270
303
  });
271
304
  }
272
- startTimer() {
273
- if (this.timer)
274
- return;
275
- this.timer = setTimeout(() => this.flush(), FLUSH_INTERVAL);
305
+ addError(entry) {
306
+ const batches = this.load();
307
+ let batch = batches.find(b => b.page === this.currentPage);
308
+ if (!batch) {
309
+ batch = { page: this.currentPage, errors: [] };
310
+ batches.push(batch);
311
+ }
312
+ batch.errors.push(entry);
313
+ if (batch.errors.length > MAX_ERRORS) {
314
+ batch.errors = batch.errors.slice(-MAX_ERRORS);
315
+ }
316
+ this.save(batches);
317
+ }
318
+ load() {
319
+ try {
320
+ const raw = localStorage.getItem(STORAGE_KEY);
321
+ return raw ? JSON.parse(raw) : [];
322
+ }
323
+ catch {
324
+ return [];
325
+ }
326
+ }
327
+ save(batches) {
328
+ try {
329
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(batches));
330
+ }
331
+ catch {
332
+ // localStorage full — noop
333
+ }
276
334
  }
277
335
  flush() {
278
- if (this.errors.length === 0)
336
+ const batches = this.load();
337
+ if (batches.length === 0)
279
338
  return;
280
- this.sendFn([...this.errors]);
281
- this.errors = [];
282
- if (this.timer) {
283
- clearTimeout(this.timer);
284
- this.timer = null;
339
+ this.sendFn(batches);
340
+ this.clearAll();
341
+ }
342
+ clearAll() {
343
+ try {
344
+ localStorage.removeItem(STORAGE_KEY);
345
+ }
346
+ catch {
347
+ // noop
285
348
  }
286
349
  }
287
350
  }
@@ -298,7 +361,8 @@ class Notix {
298
361
  this.timeout = config.timeout ?? 10000;
299
362
  this.debug = config.debug ?? false;
300
363
  this.metrika = createMetrikaTracker(config.metrika);
301
- if (config.autoCapture !== false) {
364
+ this.onCapture = config.onCapture;
365
+ if (config.autoCapture === true) {
302
366
  this.capture();
303
367
  }
304
368
  if (config.metricEnabled) {
@@ -330,8 +394,19 @@ class Notix {
330
394
  if (this.captureActive)
331
395
  return;
332
396
  initFormCapture((payload, meta) => {
333
- this.notify(payload).then(() => {
397
+ this.notify(payload).then((response) => {
334
398
  this.metrika?.trackFormCaptured(meta.title ?? payload.title, meta.fieldsCount);
399
+ // Per-form callback from data-notix-onsuccess
400
+ const form = (root instanceof Element ? root : document).querySelector('form[data-notix-bound]');
401
+ if (form instanceof HTMLFormElement && form.dataset.notixCallback) {
402
+ const fn = window[form.dataset.notixCallback];
403
+ if (typeof fn === 'function') {
404
+ fn(response);
405
+ }
406
+ delete form.dataset.notixCallback;
407
+ }
408
+ // Global callback from config
409
+ this.onCapture?.(response, payload);
335
410
  }).catch(() => { });
336
411
  }, root, this.debug);
337
412
  this.captureActive = true;
@@ -361,11 +436,13 @@ class Notix {
361
436
  enableErrorTracking() {
362
437
  if (typeof window === 'undefined')
363
438
  return;
364
- new ErrorBatcher((errors) => {
439
+ const pageDomain = location.hostname;
440
+ new ErrorBatcher((batches) => {
441
+ const totalErrors = batches.reduce((sum, b) => sum + b.errors.length, 0);
365
442
  this.notify({
366
- title: `Ошибки на странице (${errors.length})`,
443
+ title: `Ошибки на ${pageDomain}: ${totalErrors}`,
367
444
  type: 'error_batch',
368
- payload: { errors },
445
+ payload: { batches },
369
446
  }).catch(() => { });
370
447
  });
371
448
  this.log('Error tracking activated');
@@ -400,7 +477,7 @@ if (typeof document !== 'undefined') {
400
477
  const token = thisScript.dataset.token;
401
478
  if (token) {
402
479
  const endpoint = thisScript.dataset.endpoint;
403
- const autoCapture = thisScript.dataset.autoCapture !== 'false';
480
+ const autoCapture = thisScript.dataset.autoCapture === 'true';
404
481
  const debug = thisScript.dataset.debug === 'true';
405
482
  const timeout = thisScript.dataset.timeout ? parseInt(thisScript.dataset.timeout, 10) : undefined;
406
483
  const ready = document.readyState !== 'loading';
package/dist/index.mjs CHANGED
@@ -11,6 +11,7 @@ function sendWebhook(endpoint, token, payload, timeout = DEFAULT_TIMEOUT) {
11
11
  ...(payload.type !== undefined && { notification_type: payload.type }),
12
12
  ...(payload.priority !== undefined && { priority: payload.priority }),
13
13
  ...(payload.tag !== undefined && { tag: payload.tag }),
14
+ ...(payload.tags !== undefined && { tags: payload.tags }),
14
15
  ...(payload.payload !== undefined && { payload: payload.payload }),
15
16
  };
16
17
  const controller = new AbortController();
@@ -25,6 +26,7 @@ function sendWebhook(endpoint, token, payload, timeout = DEFAULT_TIMEOUT) {
25
26
  },
26
27
  body: JSON.stringify(body),
27
28
  signal: controller.signal,
29
+ keepalive: true,
28
30
  })
29
31
  .then(async (res) => {
30
32
  clearTimeout(timer);
@@ -71,17 +73,17 @@ function xhrSend(endpoint, token, body, timeout) {
71
73
 
72
74
  const SUBMIT_DEBOUNCE_MS = 500;
73
75
  const pendingForms = new WeakMap();
74
- function initFormCapture(onSubmit, root, debug) {
75
- const forms = root.querySelectorAll('form[data-notify]');
76
+ function initFormCapture(onSubmit, root, debug, onCapture) {
77
+ const forms = root.querySelectorAll('form[data-notix], form[data-notify]');
76
78
  forms.forEach((form) => {
77
79
  if (form.dataset.notixBound)
78
80
  return;
79
81
  form.dataset.notixBound = '1';
80
82
  form.addEventListener('submit', (e) => {
83
+ e.preventDefault();
81
84
  const now = Date.now();
82
85
  const last = pendingForms.get(form) ?? 0;
83
86
  if (now - last < SUBMIT_DEBOUNCE_MS) {
84
- e.preventDefault();
85
87
  return;
86
88
  }
87
89
  pendingForms.set(form, now);
@@ -90,10 +92,19 @@ function initFormCapture(onSubmit, root, debug) {
90
92
  if (debug) {
91
93
  console.log('[Notix] Form captured:', payload.title);
92
94
  }
93
- onSubmit(payload, { title: payload.title, fieldsCount: Object.keys(form.elements).length });
95
+ onSubmit(payload, { title: payload.title, fieldsCount: payload.payload ? Object.keys(payload.payload).length : 0 });
96
+ // Per-form callback: data-notix-onsuccess="funcName"
97
+ const callbackName = attr(form, 'onsuccess');
98
+ if (callbackName) {
99
+ const fn = window[callbackName];
100
+ if (typeof fn === 'function') {
101
+ // The actual callback fires after notify() resolves — handled in notix.ts
102
+ form.dataset.notixCallback = callbackName;
103
+ }
104
+ }
105
+ if (onCapture) ;
94
106
  }
95
107
  catch (err) {
96
- // don't block form submission
97
108
  if (debug) {
98
109
  console.warn('[Notix] Form capture failed:', err);
99
110
  }
@@ -104,64 +115,91 @@ function initFormCapture(onSubmit, root, debug) {
104
115
  function destroyFormCapture() {
105
116
  document.querySelectorAll('form[data-notix-bound]').forEach((form) => {
106
117
  delete form.dataset.notixBound;
107
- // listeners are removed by page navigation; for SPA use Notix.destroy() before unmount
108
118
  });
109
119
  }
120
+ function attr(form, name) {
121
+ const camel = name.charAt(0).toUpperCase() + name.slice(1);
122
+ return form.dataset[`notix${camel}`] ?? form.dataset[`notify${camel}`];
123
+ }
124
+ function elLabel(el) {
125
+ return el.getAttribute('data-notix-label') ?? el.getAttribute('data-notify-label') ?? el.name;
126
+ }
110
127
  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 tag = form.dataset.notifyTag ?? form.dataset.notify_tag;
114
- const priority = form.dataset.notifyPriority ?? form.dataset.notify_priority;
115
- const staticBody = form.dataset.notifyBody ?? form.dataset.notify_body;
116
- const template = form.dataset.notifyTemplate ?? form.dataset.notify_template;
117
- const specifiedFields = (form.dataset.notifyFields ?? form.dataset.notify_fields)
118
- ?.split(',')
119
- .map((s) => s.trim())
120
- .filter(Boolean);
121
- if (staticBody) {
122
- return {
123
- title,
124
- body: staticBody,
125
- ...(type && { type }),
126
- ...(tag && { tag }),
127
- ...(priority && { priority: priority }),
128
- };
128
+ const title = attr(form, 'title') ?? document.title;
129
+ const typeSlug = attr(form, 'type');
130
+ const tag = attr(form, 'tag');
131
+ const tagsStr = attr(form, 'tags');
132
+ const priority = attr(form, 'priority');
133
+ const bodyTemplate = attr(form, 'body');
134
+ const fieldsStr = attr(form, 'fields');
135
+ const allowedNames = new Set();
136
+ if (fieldsStr) {
137
+ fieldsStr.split(',').map(s => s.trim()).filter(Boolean).forEach(name => allowedNames.add(name));
129
138
  }
139
+ if (bodyTemplate) {
140
+ const matches = bodyTemplate.matchAll(/\{(\w+)\}/g);
141
+ for (const m of matches) {
142
+ allowedNames.add(m[1]);
143
+ }
144
+ }
145
+ let tags;
146
+ if (tag) {
147
+ tags = [tag];
148
+ }
149
+ if (tagsStr) {
150
+ try {
151
+ const parsed = JSON.parse(tagsStr);
152
+ tags = [...(tags || []), ...(Array.isArray(parsed) ? parsed : [parsed])];
153
+ }
154
+ catch {
155
+ tags = tags || [];
156
+ tags.push(tagsStr);
157
+ }
158
+ }
159
+ const fieldValues = {};
130
160
  const elements = form.querySelectorAll('input[name], textarea[name], select[name]');
131
- const fieldMap = new Map();
132
161
  elements.forEach((el) => {
133
162
  if (!el.name)
134
163
  return;
164
+ if (!allowedNames.has(el.name))
165
+ return;
135
166
  if (el.type === 'checkbox' || el.type === 'radio') {
136
167
  if (!el.checked)
137
168
  return;
138
169
  }
139
- if (specifiedFields && !specifiedFields.includes(el.name))
140
- return;
141
- const isMarked = el.hasAttribute('data-notify-field');
142
- if (specifiedFields || isMarked || (!specifiedFields && !template)) {
143
- const label = el.getAttribute('data-notify-label') ?? el.name;
144
- fieldMap.set(label, el.value || '');
145
- }
170
+ fieldValues[el.name] = el.value || '';
146
171
  });
147
172
  let body;
148
- if (template) {
149
- body = template.replace(/\{(\w+)\}/g, (_, key) => {
150
- if (fieldMap.has(key))
151
- return fieldMap.get(key);
152
- const el = form.elements.namedItem(key);
153
- return el?.value || `{${key}}`;
154
- });
173
+ if (bodyTemplate) {
174
+ body = bodyTemplate.replace(/\{(\w+)\}/g, (_, fieldName) => fieldValues[fieldName] ?? `{${fieldName}}`);
155
175
  }
156
- else if (fieldMap.size > 0) {
157
- body = Array.from(fieldMap, ([k, v]) => `${k}: ${v}`).join('\n');
176
+ const payloadData = {};
177
+ elements.forEach((el) => {
178
+ if (!el.name)
179
+ return;
180
+ if (!allowedNames.has(el.name))
181
+ return;
182
+ if ((el.type === 'checkbox' || el.type === 'radio') && !el.checked)
183
+ return;
184
+ payloadData[elLabel(el)] = el.value || '';
185
+ });
186
+ let extraPayload = {};
187
+ const payloadJson = attr(form, 'payload');
188
+ if (payloadJson) {
189
+ try {
190
+ extraPayload = JSON.parse(payloadJson);
191
+ }
192
+ catch {
193
+ // ignore invalid JSON
194
+ }
158
195
  }
159
196
  return {
160
197
  title,
161
198
  ...(body && { body }),
162
- ...(type && { type }),
163
- ...(tag && { tag }),
199
+ ...(typeSlug && { type: typeSlug }),
200
+ ...(tags && tags.length > 0 && { tags }),
164
201
  ...(priority && { priority: priority }),
202
+ payload: { ...payloadData, ...extraPayload },
165
203
  };
166
204
  }
167
205
 
@@ -222,44 +260,39 @@ class MetrikaTracker {
222
260
  }
223
261
  }
224
262
 
225
- const BATCH_SIZE = 10;
226
- const FLUSH_INTERVAL = 60000;
263
+ const STORAGE_KEY = 'notix_errors';
264
+ const MAX_ERRORS = 500;
227
265
  class ErrorBatcher {
228
266
  constructor(sendFn) {
229
267
  this.sendFn = sendFn;
230
- this.errors = [];
231
- this.timer = null;
268
+ this.currentPage = location.pathname;
232
269
  this.bindGlobal();
233
270
  }
234
271
  bindGlobal() {
235
272
  if (typeof window === 'undefined')
236
273
  return;
237
- const collect = (ev) => {
238
- this.errors.push({
274
+ const collect = (source) => (ev) => {
275
+ this.addError({
239
276
  message: ev.message || 'Unknown error',
240
277
  file: ev.filename || '',
241
278
  line: ev.lineno || 0,
242
279
  col: ev.colno || 0,
243
280
  stack: ev.error?.stack || '',
244
281
  ts: new Date().toISOString(),
282
+ source,
245
283
  });
246
- if (this.errors.length >= BATCH_SIZE)
247
- this.flush();
248
- this.startTimer();
249
284
  };
250
- window.addEventListener('error', collect);
285
+ window.addEventListener('error', collect('onerror'));
251
286
  window.addEventListener('unhandledrejection', (ev) => {
252
- this.errors.push({
287
+ this.addError({
253
288
  message: ev.reason?.message || String(ev.reason),
254
289
  file: '',
255
290
  line: 0,
256
291
  col: 0,
257
292
  stack: ev.reason?.stack || '',
258
293
  ts: new Date().toISOString(),
294
+ source: 'unhandledrejection',
259
295
  });
260
- if (this.errors.length >= BATCH_SIZE)
261
- this.flush();
262
- this.startTimer();
263
296
  });
264
297
  window.addEventListener('beforeunload', () => this.flush());
265
298
  document.addEventListener('visibilitychange', () => {
@@ -267,19 +300,49 @@ class ErrorBatcher {
267
300
  this.flush();
268
301
  });
269
302
  }
270
- startTimer() {
271
- if (this.timer)
272
- return;
273
- this.timer = setTimeout(() => this.flush(), FLUSH_INTERVAL);
303
+ addError(entry) {
304
+ const batches = this.load();
305
+ let batch = batches.find(b => b.page === this.currentPage);
306
+ if (!batch) {
307
+ batch = { page: this.currentPage, errors: [] };
308
+ batches.push(batch);
309
+ }
310
+ batch.errors.push(entry);
311
+ if (batch.errors.length > MAX_ERRORS) {
312
+ batch.errors = batch.errors.slice(-MAX_ERRORS);
313
+ }
314
+ this.save(batches);
315
+ }
316
+ load() {
317
+ try {
318
+ const raw = localStorage.getItem(STORAGE_KEY);
319
+ return raw ? JSON.parse(raw) : [];
320
+ }
321
+ catch {
322
+ return [];
323
+ }
324
+ }
325
+ save(batches) {
326
+ try {
327
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(batches));
328
+ }
329
+ catch {
330
+ // localStorage full — noop
331
+ }
274
332
  }
275
333
  flush() {
276
- if (this.errors.length === 0)
334
+ const batches = this.load();
335
+ if (batches.length === 0)
277
336
  return;
278
- this.sendFn([...this.errors]);
279
- this.errors = [];
280
- if (this.timer) {
281
- clearTimeout(this.timer);
282
- this.timer = null;
337
+ this.sendFn(batches);
338
+ this.clearAll();
339
+ }
340
+ clearAll() {
341
+ try {
342
+ localStorage.removeItem(STORAGE_KEY);
343
+ }
344
+ catch {
345
+ // noop
283
346
  }
284
347
  }
285
348
  }
@@ -296,7 +359,8 @@ class Notix {
296
359
  this.timeout = config.timeout ?? 10000;
297
360
  this.debug = config.debug ?? false;
298
361
  this.metrika = createMetrikaTracker(config.metrika);
299
- if (config.autoCapture !== false) {
362
+ this.onCapture = config.onCapture;
363
+ if (config.autoCapture === true) {
300
364
  this.capture();
301
365
  }
302
366
  if (config.metricEnabled) {
@@ -328,8 +392,19 @@ class Notix {
328
392
  if (this.captureActive)
329
393
  return;
330
394
  initFormCapture((payload, meta) => {
331
- this.notify(payload).then(() => {
395
+ this.notify(payload).then((response) => {
332
396
  this.metrika?.trackFormCaptured(meta.title ?? payload.title, meta.fieldsCount);
397
+ // Per-form callback from data-notix-onsuccess
398
+ const form = (root instanceof Element ? root : document).querySelector('form[data-notix-bound]');
399
+ if (form instanceof HTMLFormElement && form.dataset.notixCallback) {
400
+ const fn = window[form.dataset.notixCallback];
401
+ if (typeof fn === 'function') {
402
+ fn(response);
403
+ }
404
+ delete form.dataset.notixCallback;
405
+ }
406
+ // Global callback from config
407
+ this.onCapture?.(response, payload);
333
408
  }).catch(() => { });
334
409
  }, root, this.debug);
335
410
  this.captureActive = true;
@@ -359,11 +434,13 @@ class Notix {
359
434
  enableErrorTracking() {
360
435
  if (typeof window === 'undefined')
361
436
  return;
362
- new ErrorBatcher((errors) => {
437
+ const pageDomain = location.hostname;
438
+ new ErrorBatcher((batches) => {
439
+ const totalErrors = batches.reduce((sum, b) => sum + b.errors.length, 0);
363
440
  this.notify({
364
- title: `Ошибки на странице (${errors.length})`,
441
+ title: `Ошибки на ${pageDomain}: ${totalErrors}`,
365
442
  type: 'error_batch',
366
- payload: { errors },
443
+ payload: { batches },
367
444
  }).catch(() => { });
368
445
  });
369
446
  this.log('Error tracking activated');
@@ -398,7 +475,7 @@ if (typeof document !== 'undefined') {
398
475
  const token = thisScript.dataset.token;
399
476
  if (token) {
400
477
  const endpoint = thisScript.dataset.endpoint;
401
- const autoCapture = thisScript.dataset.autoCapture !== 'false';
478
+ const autoCapture = thisScript.dataset.autoCapture === 'true';
402
479
  const debug = thisScript.dataset.debug === 'true';
403
480
  const timeout = thisScript.dataset.timeout ? parseInt(thisScript.dataset.timeout, 10) : undefined;
404
481
  const ready = document.readyState !== 'loading';
@@ -3,4 +3,4 @@
3
3
  * @version <%= pkg.version %>
4
4
  * @license MIT
5
5
  */
6
- var Notix=function(t){"use strict";function e(t,e,r,i=1e4){const o={title:r.title,...void 0!==r.body&&{body:r.body},...void 0!==r.type&&{notification_type:r.type},...void 0!==r.priority&&{priority:r.priority},...void 0!==r.tag&&{tag:r.tag},...void 0!==r.payload&&{payload:r.payload}},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(o),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,r,i){return new Promise((o,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?o(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(r))})}(t,e,o,i)}const r=new WeakMap;function i(t,e,i){e.querySelectorAll("form[data-notify]").forEach(e=>{e.dataset.notixBound||(e.dataset.notixBound="1",e.addEventListener("submit",o=>{const n=Date.now();if(n-(r.get(e)??0)<500)o.preventDefault();else{r.set(e,n);try{const r=function(t){const e=t.dataset.notifyTitle??t.dataset.notify_title??document.title,r=t.dataset.notifyType??t.dataset.notify_type,i=t.dataset.notifyTag??t.dataset.notify_tag,o=t.dataset.notifyPriority??t.dataset.notify_priority,n=t.dataset.notifyBody??t.dataset.notify_body,a=t.dataset.notifyTemplate??t.dataset.notify_template,s=(t.dataset.notifyFields??t.dataset.notify_fields)?.split(",").map(t=>t.trim()).filter(Boolean);if(n)return{title:e,body:n,...r&&{type:r},...i&&{tag:i},...o&&{priority:o}};const d=t.querySelectorAll("input[name], textarea[name], select[name]"),c=new Map;let l;d.forEach(t=>{if(!t.name)return;if(("checkbox"===t.type||"radio"===t.type)&&!t.checked)return;if(s&&!s.includes(t.name))return;const e=t.hasAttribute("data-notify-field");if(s||e||!s&&!a){const e=t.getAttribute("data-notify-label")??t.name;c.set(e,t.value||"")}}),a?l=a.replace(/\{(\w+)\}/g,(e,r)=>{if(c.has(r))return c.get(r);const i=t.elements.namedItem(r);return i?.value||`{${r}}`}):c.size>0&&(l=Array.from(c,([t,e])=>`${t}: ${e}`).join("\n"));return{title:e,...l&&{body:l},...r&&{type:r},...i&&{tag:i},...o&&{priority:o}}}(e);i&&console.log("[Notix] Form captured:",r.title),t(r,{title:r.title,fieldsCount:Object.keys(e.elements).length})}catch(t){i&&console.warn("[Notix] Form capture failed:",t)}}}))})}function o(t){if(!1===t?.enabled)return null;const e="undefined"!=typeof window&&"function"==typeof window.ym;if(!e&&!t?.enabled)return null;let r=t?.counterId;return!r&&e&&(r=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}()),r?new n(r,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){this.sendFn=t,this.errors=[],this.timer=null,this.bindGlobal()}bindGlobal(){if("undefined"==typeof window)return;window.addEventListener("error",t=>{this.errors.push({message:t.message||"Unknown error",file:t.filename||"",line:t.lineno||0,col:t.colno||0,stack:t.error?.stack||"",ts:(new Date).toISOString()}),this.errors.length>=10&&this.flush(),this.startTimer()}),window.addEventListener("unhandledrejection",t=>{this.errors.push({message:t.reason?.message||String(t.reason),file:"",line:0,col:0,stack:t.reason?.stack||"",ts:(new Date).toISOString()}),this.errors.length>=10&&this.flush(),this.startTimer()}),window.addEventListener("beforeunload",()=>this.flush()),document.addEventListener("visibilitychange",()=>{"hidden"===document.visibilityState&&this.flush()})}startTimer(){this.timer||(this.timer=setTimeout(()=>this.flush(),6e4))}flush(){0!==this.errors.length&&(this.sendFn([...this.errors]),this.errors=[],this.timer&&(clearTimeout(this.timer),this.timer=null))}}class s{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=o(t.metrika),!1!==t.autoCapture&&this.capture(),t.metricEnabled&&this.sendPageview(),t.errorTrackingEnabled&&this.enableErrorTracking()}async notify(t){if(!t.title)throw new Error("Notix: title is required");this.log("Sending notification:",t.title);try{const r=await e(this.endpoint,this.token,t,this.timeout);return this.log("Notification sent:",r.id),this.metrika?.trackSent(r),r}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")}sendPageview(t,e){const r={title:"Посетитель на сайте",type:"metric",payload:{event_type:"pageview",visitor_id:c(),page:t||location.pathname,referrer:e||document.referrer||void 0,value:1}};this.notify(r).catch(()=>{}),this.log("Pageview sent:",t||location.pathname)}enableErrorTracking(){"undefined"!=typeof window&&(new a(t=>{this.notify({title:`Ошибки на странице (${t.length})`,type:"error_batch",payload:{errors:t}}).catch(()=>{})}),this.log("Error tracking activated"))}static getVisitorId(){return c()}log(...t){this.debug&&console.log("[Notix]",...t)}}const d="notix_vid";function c(){try{let t=localStorage.getItem(d);return t||(t=crypto.randomUUID(),localStorage.setItem(d,t)),t}catch{return"unknown"}}if("undefined"!=typeof document){const t=document.currentScript;if(t){const e=t.dataset.token;if(e){const r=t.dataset.endpoint,i="false"!==t.dataset.autoCapture,o="true"===t.dataset.debug,n=t.dataset.timeout?parseInt(t.dataset.timeout,10):void 0,a=()=>{const a="true"===t.dataset.metric,d="true"===t.dataset.errors,c=new s({token:e,...r&&{endpoint:r},autoCapture:i,debug:o,metricEnabled:a,errorTrackingEnabled:d,...n&&{timeout:n}});window.notix=c};"loading"!==document.readyState?a():document.addEventListener("DOMContentLoaded",a)}else"true"===t.dataset.debug&&console.warn("[Notix] data-token attribute is required on script tag")}}return t.Notix=s,t}({});
6
+ var Notix=function(t){"use strict";function e(t,e,o,r=1e4){const n={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},...void 0!==o.tag&&{tag:o.tag},...void 0!==o.tags&&{tags:o.tags},...void 0!==o.payload&&{payload:o.payload}},a=new AbortController,i=setTimeout(()=>a.abort(),r);return"undefined"!=typeof fetch?fetch(t,{method:"POST",headers:{Authorization:`Bearer ${e}`,"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(n),signal:a.signal,keepalive:!0}).then(async t=>{clearTimeout(i);const e=await t.json();if(!t.ok)throw new Error(e.error||e.message||`HTTP ${t.status}`);return e}).catch(t=>{throw clearTimeout(i),t}):function(t,e,o,r){return new Promise((n,a)=>{const i=new XMLHttpRequest;i.open("POST",t,!0),i.setRequestHeader("Authorization",`Bearer ${e}`),i.setRequestHeader("Content-Type","application/json"),i.setRequestHeader("Accept","application/json"),i.timeout=r,i.onload=()=>{try{const t=JSON.parse(i.responseText);i.status>=200&&i.status<300?n(t):a(new Error(t.error||t.message||`HTTP ${i.status}`))}catch{a(new Error("Invalid response"))}},i.onerror=()=>a(new Error("Network error")),i.ontimeout=()=>a(new Error("Request timeout")),i.send(JSON.stringify(o))})}(t,e,n,r)}const o=new WeakMap;function r(t,e,r,a){e.querySelectorAll("form[data-notix], form[data-notify]").forEach(e=>{e.dataset.notixBound||(e.dataset.notixBound="1",e.addEventListener("submit",a=>{a.preventDefault();const i=Date.now();if(!(i-(o.get(e)??0)<500)){o.set(e,i);try{const o=function(t){const e=n(t,"title")??document.title,o=n(t,"type"),r=n(t,"tag"),a=n(t,"tags"),i=n(t,"priority"),s=n(t,"body"),c=n(t,"fields"),d=new Set;c&&c.split(",").map(t=>t.trim()).filter(Boolean).forEach(t=>d.add(t));if(s){const t=s.matchAll(/\{(\w+)\}/g);for(const e of t)d.add(e[1])}let l;r&&(l=[r]);if(a)try{const t=JSON.parse(a);l=[...l||[],...Array.isArray(t)?t:[t]]}catch{l=l||[],l.push(a)}const u={},h=t.querySelectorAll("input[name], textarea[name], select[name]");let p;h.forEach(t=>{t.name&&d.has(t.name)&&("checkbox"!==t.type&&"radio"!==t.type||t.checked)&&(u[t.name]=t.value||"")}),s&&(p=s.replace(/\{(\w+)\}/g,(t,e)=>u[e]??`{${e}}`));const f={};h.forEach(t=>{t.name&&d.has(t.name)&&("checkbox"!==t.type&&"radio"!==t.type||t.checked)&&(f[function(t){return t.getAttribute("data-notix-label")??t.getAttribute("data-notify-label")??t.name}(t)]=t.value||"")});let m={};const g=n(t,"payload");if(g)try{m=JSON.parse(g)}catch{}return{title:e,...p&&{body:p},...o&&{type:o},...l&&l.length>0&&{tags:l},...i&&{priority:i},payload:{...f,...m}}}(e);r&&console.log("[Notix] Form captured:",o.title),t(o,{title:o.title,fieldsCount:o.payload?Object.keys(o.payload).length:0});const a=n(e,"onsuccess");if(a){"function"==typeof window[a]&&(e.dataset.notixCallback=a)}}catch(t){r&&console.warn("[Notix] Form capture failed:",t)}}}))})}function n(t,e){const o=e.charAt(0).toUpperCase()+e.slice(1);return t.dataset[`notix${o}`]??t.dataset[`notify${o}`]}function a(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 i(o,t?.prefix??"notix"):null}class i{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)}}const s="notix_errors";class c{constructor(t){this.sendFn=t,this.currentPage=location.pathname,this.bindGlobal()}bindGlobal(){if("undefined"==typeof window)return;window.addEventListener("error",(t=>e=>{this.addError({message:e.message||"Unknown error",file:e.filename||"",line:e.lineno||0,col:e.colno||0,stack:e.error?.stack||"",ts:(new Date).toISOString(),source:t})})("onerror")),window.addEventListener("unhandledrejection",t=>{this.addError({message:t.reason?.message||String(t.reason),file:"",line:0,col:0,stack:t.reason?.stack||"",ts:(new Date).toISOString(),source:"unhandledrejection"})}),window.addEventListener("beforeunload",()=>this.flush()),document.addEventListener("visibilitychange",()=>{"hidden"===document.visibilityState&&this.flush()})}addError(t){const e=this.load();let o=e.find(t=>t.page===this.currentPage);o||(o={page:this.currentPage,errors:[]},e.push(o)),o.errors.push(t),o.errors.length>500&&(o.errors=o.errors.slice(-500)),this.save(e)}load(){try{const t=localStorage.getItem(s);return t?JSON.parse(t):[]}catch{return[]}}save(t){try{localStorage.setItem(s,JSON.stringify(t))}catch{}}flush(){const t=this.load();0!==t.length&&(this.sendFn(t),this.clearAll())}clearAll(){try{localStorage.removeItem(s)}catch{}}}class d{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=a(t.metrika),this.onCapture=t.onCapture,!0===t.autoCapture&&this.capture(),t.metricEnabled&&this.sendPageview(),t.errorTrackingEnabled&&this.enableErrorTracking()}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||(r((e,o)=>{this.notify(e).then(r=>{this.metrika?.trackFormCaptured(o.title??e.title,o.fieldsCount);const n=(t instanceof Element?t:document).querySelector("form[data-notix-bound]");if(n instanceof HTMLFormElement&&n.dataset.notixCallback){const t=window[n.dataset.notixCallback];"function"==typeof t&&t(r),delete n.dataset.notixCallback}this.onCapture?.(r,e)}).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")}sendPageview(t,e){const o={title:"Посетитель на сайте",type:"metric",payload:{event_type:"pageview",visitor_id:u(),page:t||location.pathname,referrer:e||document.referrer||void 0,value:1}};this.notify(o).catch(()=>{}),this.log("Pageview sent:",t||location.pathname)}enableErrorTracking(){if("undefined"==typeof window)return;const t=location.hostname;new c(e=>{const o=e.reduce((t,e)=>t+e.errors.length,0);this.notify({title:`Ошибки на ${t}: ${o}`,type:"error_batch",payload:{batches:e}}).catch(()=>{})}),this.log("Error tracking activated")}static getVisitorId(){return u()}log(...t){this.debug&&console.log("[Notix]",...t)}}const l="notix_vid";function u(){try{let t=localStorage.getItem(l);return t||(t=crypto.randomUUID(),localStorage.setItem(l,t)),t}catch{return"unknown"}}if("undefined"!=typeof document){const t=document.currentScript;if(t){const e=t.dataset.token;if(e){const o=t.dataset.endpoint,r="true"===t.dataset.autoCapture,n="true"===t.dataset.debug,a=t.dataset.timeout?parseInt(t.dataset.timeout,10):void 0,i=()=>{const i="true"===t.dataset.metric,s="true"===t.dataset.errors,c=new d({token:e,...o&&{endpoint:o},autoCapture:r,debug:n,metricEnabled:i,errorTrackingEnabled:s,...a&&{timeout:a}});window.notix=c};"loading"!==document.readyState?i():document.addEventListener("DOMContentLoaded",i)}else"true"===t.dataset.debug&&console.warn("[Notix] data-token attribute is required on script tag")}}return t.Notix=d,t}({});
package/dist/notix.d.ts CHANGED
@@ -6,6 +6,7 @@ export declare class Notix {
6
6
  private debug;
7
7
  private metrika;
8
8
  private captureActive;
9
+ private onCapture?;
9
10
  constructor(config: NotixConfig);
10
11
  notify(payload: NotifyPayload): Promise<{
11
12
  message: string;
package/dist/types.d.ts CHANGED
@@ -1,15 +1,25 @@
1
1
  export type NotificationPriority = 'low' | 'normal' | 'high' | 'urgent';
2
+ export interface TagObject {
3
+ name: string;
4
+ color?: string;
5
+ }
2
6
  export interface NotifyPayload {
3
7
  title: string;
4
8
  body?: string;
5
9
  type?: string;
6
10
  tag?: string;
11
+ tags?: Array<string | TagObject>;
7
12
  priority?: NotificationPriority;
8
13
  payload?: Record<string, unknown>;
9
14
  }
10
- export interface NotifyRequestBody extends NotifyPayload {
15
+ export interface NotifyRequestBody {
16
+ title: string;
17
+ body?: string;
11
18
  notification_type?: string;
12
19
  tag?: string;
20
+ tags?: Array<string | TagObject>;
21
+ priority?: string;
22
+ payload?: Record<string, unknown>;
13
23
  }
14
24
  export interface NotixResponse {
15
25
  message: string;
@@ -29,4 +39,5 @@ export interface NotixConfig {
29
39
  metrika?: MetrikaConfig;
30
40
  timeout?: number;
31
41
  debug?: boolean;
42
+ onCapture?: (response: NotixResponse, payload: NotifyPayload) => void;
32
43
  }
package/package.json CHANGED
@@ -1,40 +1,40 @@
1
- {
2
- "name": "@notix-hub/sdk",
3
- "version": "0.3.1",
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
- }
1
+ {
2
+ "name": "@notix-hub/sdk",
3
+ "version": "0.3.3",
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
+ }