@notix-hub/sdk 0.3.0 → 0.3.2

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/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));
140
+ }
141
+ if (bodyTemplate) {
142
+ const matches = bodyTemplate.matchAll(/\{(\w+)\}/g);
143
+ for (const m of matches) {
144
+ allowedNames.add(m[1]);
145
+ }
131
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
 
@@ -298,10 +336,16 @@ class Notix {
298
336
  this.timeout = config.timeout ?? 10000;
299
337
  this.debug = config.debug ?? false;
300
338
  this.metrika = createMetrikaTracker(config.metrika);
301
- if (config.autoCapture !== false) {
339
+ this.onCapture = config.onCapture;
340
+ if (config.autoCapture === true) {
302
341
  this.capture();
303
342
  }
304
- this.trackErrors();
343
+ if (config.metricEnabled) {
344
+ this.sendPageview();
345
+ }
346
+ if (config.errorTrackingEnabled) {
347
+ this.enableErrorTracking();
348
+ }
305
349
  }
306
350
  async notify(payload) {
307
351
  if (!payload.title) {
@@ -325,8 +369,19 @@ class Notix {
325
369
  if (this.captureActive)
326
370
  return;
327
371
  initFormCapture((payload, meta) => {
328
- this.notify(payload).then(() => {
372
+ this.notify(payload).then((response) => {
329
373
  this.metrika?.trackFormCaptured(meta.title ?? payload.title, meta.fieldsCount);
374
+ // Per-form callback from data-notix-onsuccess
375
+ const form = (root instanceof Element ? root : document).querySelector('form[data-notix-bound]');
376
+ if (form instanceof HTMLFormElement && form.dataset.notixCallback) {
377
+ const fn = window[form.dataset.notixCallback];
378
+ if (typeof fn === 'function') {
379
+ fn(response);
380
+ }
381
+ delete form.dataset.notixCallback;
382
+ }
383
+ // Global callback from config
384
+ this.onCapture?.(response, payload);
330
385
  }).catch(() => { });
331
386
  }, root, this.debug);
332
387
  this.captureActive = true;
@@ -353,7 +408,7 @@ class Notix {
353
408
  this.notify(payload).catch(() => { });
354
409
  this.log('Pageview sent:', page || location.pathname);
355
410
  }
356
- trackErrors() {
411
+ enableErrorTracking() {
357
412
  if (typeof window === 'undefined')
358
413
  return;
359
414
  new ErrorBatcher((errors) => {
@@ -395,16 +450,20 @@ if (typeof document !== 'undefined') {
395
450
  const token = thisScript.dataset.token;
396
451
  if (token) {
397
452
  const endpoint = thisScript.dataset.endpoint;
398
- const autoCapture = thisScript.dataset.autoCapture !== 'false';
453
+ const autoCapture = thisScript.dataset.autoCapture === 'true';
399
454
  const debug = thisScript.dataset.debug === 'true';
400
455
  const timeout = thisScript.dataset.timeout ? parseInt(thisScript.dataset.timeout, 10) : undefined;
401
456
  const ready = document.readyState !== 'loading';
402
457
  const init = () => {
458
+ const metricEnabled = thisScript.dataset.metric === 'true';
459
+ const errorTrackingEnabled = thisScript.dataset.errors === 'true';
403
460
  const notix = new Notix({
404
461
  token,
405
462
  ...(endpoint && { endpoint }),
406
463
  autoCapture,
407
464
  debug,
465
+ metricEnabled,
466
+ errorTrackingEnabled,
408
467
  ...(timeout && { timeout }),
409
468
  });
410
469
  window.notix = notix;
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));
138
+ }
139
+ if (bodyTemplate) {
140
+ const matches = bodyTemplate.matchAll(/\{(\w+)\}/g);
141
+ for (const m of matches) {
142
+ allowedNames.add(m[1]);
143
+ }
129
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
 
@@ -296,10 +334,16 @@ class Notix {
296
334
  this.timeout = config.timeout ?? 10000;
297
335
  this.debug = config.debug ?? false;
298
336
  this.metrika = createMetrikaTracker(config.metrika);
299
- if (config.autoCapture !== false) {
337
+ this.onCapture = config.onCapture;
338
+ if (config.autoCapture === true) {
300
339
  this.capture();
301
340
  }
302
- this.trackErrors();
341
+ if (config.metricEnabled) {
342
+ this.sendPageview();
343
+ }
344
+ if (config.errorTrackingEnabled) {
345
+ this.enableErrorTracking();
346
+ }
303
347
  }
304
348
  async notify(payload) {
305
349
  if (!payload.title) {
@@ -323,8 +367,19 @@ class Notix {
323
367
  if (this.captureActive)
324
368
  return;
325
369
  initFormCapture((payload, meta) => {
326
- this.notify(payload).then(() => {
370
+ this.notify(payload).then((response) => {
327
371
  this.metrika?.trackFormCaptured(meta.title ?? payload.title, meta.fieldsCount);
372
+ // Per-form callback from data-notix-onsuccess
373
+ const form = (root instanceof Element ? root : document).querySelector('form[data-notix-bound]');
374
+ if (form instanceof HTMLFormElement && form.dataset.notixCallback) {
375
+ const fn = window[form.dataset.notixCallback];
376
+ if (typeof fn === 'function') {
377
+ fn(response);
378
+ }
379
+ delete form.dataset.notixCallback;
380
+ }
381
+ // Global callback from config
382
+ this.onCapture?.(response, payload);
328
383
  }).catch(() => { });
329
384
  }, root, this.debug);
330
385
  this.captureActive = true;
@@ -351,7 +406,7 @@ class Notix {
351
406
  this.notify(payload).catch(() => { });
352
407
  this.log('Pageview sent:', page || location.pathname);
353
408
  }
354
- trackErrors() {
409
+ enableErrorTracking() {
355
410
  if (typeof window === 'undefined')
356
411
  return;
357
412
  new ErrorBatcher((errors) => {
@@ -393,16 +448,20 @@ if (typeof document !== 'undefined') {
393
448
  const token = thisScript.dataset.token;
394
449
  if (token) {
395
450
  const endpoint = thisScript.dataset.endpoint;
396
- const autoCapture = thisScript.dataset.autoCapture !== 'false';
451
+ const autoCapture = thisScript.dataset.autoCapture === 'true';
397
452
  const debug = thisScript.dataset.debug === 'true';
398
453
  const timeout = thisScript.dataset.timeout ? parseInt(thisScript.dataset.timeout, 10) : undefined;
399
454
  const ready = document.readyState !== 'loading';
400
455
  const init = () => {
456
+ const metricEnabled = thisScript.dataset.metric === 'true';
457
+ const errorTrackingEnabled = thisScript.dataset.errors === 'true';
401
458
  const notix = new Notix({
402
459
  token,
403
460
  ...(endpoint && { endpoint }),
404
461
  autoCapture,
405
462
  debug,
463
+ metricEnabled,
464
+ errorTrackingEnabled,
406
465
  ...(timeout && { timeout }),
407
466
  });
408
467
  window.notix = notix;
@@ -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,i,r=1e4){const o={title:i.title,...void 0!==i.body&&{body:i.body},...void 0!==i.type&&{notification_type:i.type},...void 0!==i.priority&&{priority:i.priority},...void 0!==i.tag&&{tag:i.tag},...void 0!==i.payload&&{payload:i.payload}},n=new AbortController,a=setTimeout(()=>n.abort(),r);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,i,r){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=r,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(i))})}(t,e,o,r)}const i=new WeakMap;function r(t,e,r){e.querySelectorAll("form[data-notify]").forEach(e=>{e.dataset.notixBound||(e.dataset.notixBound="1",e.addEventListener("submit",o=>{const n=Date.now();if(n-(i.get(e)??0)<500)o.preventDefault();else{i.set(e,n);try{const i=function(t){const e=t.dataset.notifyTitle??t.dataset.notify_title??document.title,i=t.dataset.notifyType??t.dataset.notify_type,r=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,...i&&{type:i},...r&&{tag:r},...o&&{priority:o}};const d=t.querySelectorAll("input[name], textarea[name], select[name]"),c=new Map;let u;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?u=a.replace(/\{(\w+)\}/g,(e,i)=>{if(c.has(i))return c.get(i);const r=t.elements.namedItem(i);return r?.value||`{${i}}`}):c.size>0&&(u=Array.from(c,([t,e])=>`${t}: ${e}`).join("\n"));return{title:e,...u&&{body:u},...i&&{type:i},...r&&{tag:r},...o&&{priority:o}}}(e);r&&console.log("[Notix] Form captured:",i.title),t(i,{title:i.title,fieldsCount:Object.keys(e.elements).length})}catch(t){r&&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 i=t?.counterId;return!i&&e&&(i=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}()),i?new n(i,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(),this.trackErrors()}async notify(t){if(!t.title)throw new Error("Notix: title is required");this.log("Sending notification:",t.title);try{const i=await e(this.endpoint,this.token,t,this.timeout);return this.log("Notification sent:",i.id),this.metrika?.trackSent(i),i}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((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 i={title:"Посетитель на сайте",type:"metric",payload:{event_type:"pageview",visitor_id:c(),page:t||location.pathname,referrer:e||document.referrer||void 0,value:1}};this.notify(i).catch(()=>{}),this.log("Pageview sent:",t||location.pathname)}trackErrors(){"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 i=t.dataset.endpoint,r="false"!==t.dataset.autoCapture,o="true"===t.dataset.debug,n=t.dataset.timeout?parseInt(t.dataset.timeout,10):void 0,a=()=>{const t=new s({token:e,...i&&{endpoint:i},autoCapture:r,debug:o,...n&&{timeout:n}});window.notix=t};"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 i={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}},n=new AbortController,a=setTimeout(()=>n.abort(),r);return"undefined"!=typeof fetch?fetch(t,{method:"POST",headers:{Authorization:`Bearer ${e}`,"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(i),signal:n.signal,keepalive:!0}).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,r){return new Promise((i,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=r,a.onload=()=>{try{const t=JSON.parse(a.responseText);a.status>=200&&a.status<300?i(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,i,r)}const o=new WeakMap;function r(t,e,r,n){e.querySelectorAll("form[data-notix], form[data-notify]").forEach(e=>{e.dataset.notixBound||(e.dataset.notixBound="1",e.addEventListener("submit",n=>{n.preventDefault();const a=Date.now();if(!(a-(o.get(e)??0)<500)){o.set(e,a);try{const o=function(t){const e=i(t,"title")??document.title,o=i(t,"type"),r=i(t,"tag"),n=i(t,"tags"),a=i(t,"priority"),s=i(t,"body"),c=i(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(n)try{const t=JSON.parse(n);l=[...l||[],...Array.isArray(t)?t:[t]]}catch{l=l||[],l.push(n)}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 y=i(t,"payload");if(y)try{m=JSON.parse(y)}catch{}return{title:e,...p&&{body:p},...o&&{type:o},...l&&l.length>0&&{tags:l},...a&&{priority:a},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 n=i(e,"onsuccess");if(n){"function"==typeof window[n]&&(e.dataset.notixCallback=n)}}catch(t){r&&console.warn("[Notix] Form capture failed:",t)}}}))})}function i(t,e){const o=e.charAt(0).toUpperCase()+e.slice(1);return t.dataset[`notix${o}`]??t.dataset[`notify${o}`]}function n(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 a(o,t?.prefix??"notix"):null}class a{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 s{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 c{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=n(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 i=(t instanceof Element?t:document).querySelector("form[data-notix-bound]");if(i instanceof HTMLFormElement&&i.dataset.notixCallback){const t=window[i.dataset.notixCallback];"function"==typeof t&&t(r),delete i.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:l(),page:t||location.pathname,referrer:e||document.referrer||void 0,value:1}};this.notify(o).catch(()=>{}),this.log("Pageview sent:",t||location.pathname)}enableErrorTracking(){"undefined"!=typeof window&&(new s(t=>{this.notify({title:`Ошибки на странице (${t.length})`,type:"error_batch",payload:{errors:t}}).catch(()=>{})}),this.log("Error tracking activated"))}static getVisitorId(){return l()}log(...t){this.debug&&console.log("[Notix]",...t)}}const d="notix_vid";function l(){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 o=t.dataset.endpoint,r="true"===t.dataset.autoCapture,i="true"===t.dataset.debug,n=t.dataset.timeout?parseInt(t.dataset.timeout,10):void 0,a=()=>{const a="true"===t.dataset.metric,s="true"===t.dataset.errors,d=new c({token:e,...o&&{endpoint:o},autoCapture:r,debug:i,metricEnabled:a,errorTrackingEnabled:s,...n&&{timeout:n}});window.notix=d};"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=c,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;
@@ -14,7 +15,7 @@ export declare class Notix {
14
15
  capture(root?: Element | Document): void;
15
16
  destroy(): void;
16
17
  sendPageview(page?: string, referrer?: string): void;
17
- trackErrors(): void;
18
+ enableErrorTracking(): void;
18
19
  static getVisitorId(): string;
19
20
  private log;
20
21
  }
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;
@@ -24,7 +34,10 @@ export interface NotixConfig {
24
34
  token: string;
25
35
  endpoint?: string;
26
36
  autoCapture?: boolean;
37
+ metricEnabled?: boolean;
38
+ errorTrackingEnabled?: boolean;
27
39
  metrika?: MetrikaConfig;
28
40
  timeout?: number;
29
41
  debug?: boolean;
42
+ onCapture?: (response: NotixResponse, payload: NotifyPayload) => void;
30
43
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notix-hub/sdk",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "JavaScript SDK for Notix (Нотикс) — notification aggregation service",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",