@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 +2 -2
- package/dist/index.cjs +107 -48
- package/dist/index.mjs +107 -48
- package/dist/notify.min.js +1 -1
- package/dist/notix.d.ts +2 -1
- package/dist/types.d.ts +14 -1
- package/package.json +1 -1
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(
|
|
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
|
|
114
|
-
const
|
|
115
|
-
const tag = form
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
.filter(Boolean);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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 (
|
|
151
|
-
body =
|
|
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
|
-
|
|
159
|
-
|
|
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
|
-
...(
|
|
165
|
-
...(
|
|
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
|
-
|
|
339
|
+
this.onCapture = config.onCapture;
|
|
340
|
+
if (config.autoCapture === true) {
|
|
302
341
|
this.capture();
|
|
303
342
|
}
|
|
304
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
112
|
-
const
|
|
113
|
-
const tag = form
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
.filter(Boolean);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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 (
|
|
149
|
-
body =
|
|
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
|
-
|
|
157
|
-
|
|
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
|
-
...(
|
|
163
|
-
...(
|
|
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
|
-
|
|
337
|
+
this.onCapture = config.onCapture;
|
|
338
|
+
if (config.autoCapture === true) {
|
|
300
339
|
this.capture();
|
|
301
340
|
}
|
|
302
|
-
|
|
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
|
-
|
|
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
|
|
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;
|
package/dist/notify.min.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
|
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
|
}
|