@notix-hub/sdk 0.3.1 → 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,7 +336,8 @@ 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
343
  if (config.metricEnabled) {
@@ -330,8 +369,19 @@ class Notix {
330
369
  if (this.captureActive)
331
370
  return;
332
371
  initFormCapture((payload, meta) => {
333
- this.notify(payload).then(() => {
372
+ this.notify(payload).then((response) => {
334
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);
335
385
  }).catch(() => { });
336
386
  }, root, this.debug);
337
387
  this.captureActive = true;
@@ -400,7 +450,7 @@ if (typeof document !== 'undefined') {
400
450
  const token = thisScript.dataset.token;
401
451
  if (token) {
402
452
  const endpoint = thisScript.dataset.endpoint;
403
- const autoCapture = thisScript.dataset.autoCapture !== 'false';
453
+ const autoCapture = thisScript.dataset.autoCapture === 'true';
404
454
  const debug = thisScript.dataset.debug === 'true';
405
455
  const timeout = thisScript.dataset.timeout ? parseInt(thisScript.dataset.timeout, 10) : undefined;
406
456
  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));
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,7 +334,8 @@ 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
341
  if (config.metricEnabled) {
@@ -328,8 +367,19 @@ class Notix {
328
367
  if (this.captureActive)
329
368
  return;
330
369
  initFormCapture((payload, meta) => {
331
- this.notify(payload).then(() => {
370
+ this.notify(payload).then((response) => {
332
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);
333
383
  }).catch(() => { });
334
384
  }, root, this.debug);
335
385
  this.captureActive = true;
@@ -398,7 +448,7 @@ if (typeof document !== 'undefined') {
398
448
  const token = thisScript.dataset.token;
399
449
  if (token) {
400
450
  const endpoint = thisScript.dataset.endpoint;
401
- const autoCapture = thisScript.dataset.autoCapture !== 'false';
451
+ const autoCapture = thisScript.dataset.autoCapture === 'true';
402
452
  const debug = thisScript.dataset.debug === 'true';
403
453
  const timeout = thisScript.dataset.timeout ? parseInt(thisScript.dataset.timeout, 10) : undefined;
404
454
  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 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;
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@notix-hub/sdk",
3
- "version": "0.3.1",
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",