@reachy/audience-module 1.0.12 → 1.0.14
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.
|
@@ -2,6 +2,288 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.V2AudienceEngine = void 0;
|
|
4
4
|
const CriteriaParser_1 = require("../builders/CriteriaParser");
|
|
5
|
+
const getByPath = (obj, dottedPath) => {
|
|
6
|
+
if (!obj || !dottedPath)
|
|
7
|
+
return undefined;
|
|
8
|
+
const parts = dottedPath.split('.').filter(Boolean);
|
|
9
|
+
let cur = obj;
|
|
10
|
+
for (const p of parts) {
|
|
11
|
+
if (cur == null)
|
|
12
|
+
return undefined;
|
|
13
|
+
cur = cur[p];
|
|
14
|
+
}
|
|
15
|
+
return cur;
|
|
16
|
+
};
|
|
17
|
+
const getEventPropertyValue = (eventData, keyRaw) => {
|
|
18
|
+
const key = String(keyRaw || '').trim();
|
|
19
|
+
if (!key)
|
|
20
|
+
return undefined;
|
|
21
|
+
const ed = eventData || {};
|
|
22
|
+
if (key.startsWith('event_data.'))
|
|
23
|
+
return getByPath(ed, key.replace(/^event_data\./, ''));
|
|
24
|
+
if (key.startsWith('custom_data.'))
|
|
25
|
+
return getByPath(ed?.custom_data, key.replace(/^custom_data\./, ''));
|
|
26
|
+
if (key.startsWith('utm_data.'))
|
|
27
|
+
return getByPath(ed?.utm_data, key.replace(/^utm_data\./, ''));
|
|
28
|
+
if (key.startsWith('url_data.'))
|
|
29
|
+
return getByPath(ed?.url_data, key.replace(/^url_data\./, ''));
|
|
30
|
+
if (key.startsWith('session_data.'))
|
|
31
|
+
return getByPath(ed?.session_data, key.replace(/^session_data\./, ''));
|
|
32
|
+
return getByPath(ed, key) ?? getByPath(ed?.custom_data, key);
|
|
33
|
+
};
|
|
34
|
+
const isValidIanaTimezone = (tzRaw) => {
|
|
35
|
+
const tz = String(tzRaw || '').trim();
|
|
36
|
+
if (!tz)
|
|
37
|
+
return false;
|
|
38
|
+
try {
|
|
39
|
+
new Intl.DateTimeFormat('en-US', { timeZone: tz });
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const parseTimeToMinutes = (raw) => {
|
|
47
|
+
const s = String(raw || '').trim();
|
|
48
|
+
if (!s)
|
|
49
|
+
return null;
|
|
50
|
+
const m = s.match(/^(\d{1,2}):(\d{2})\s*(AM|PM)?$/i);
|
|
51
|
+
if (!m)
|
|
52
|
+
return null;
|
|
53
|
+
let hh = Number(m[1]);
|
|
54
|
+
const mm = Number(m[2]);
|
|
55
|
+
const ampm = m[3] ? String(m[3]).toUpperCase() : null;
|
|
56
|
+
if (!Number.isFinite(hh) || !Number.isFinite(mm))
|
|
57
|
+
return null;
|
|
58
|
+
if (mm < 0 || mm > 59)
|
|
59
|
+
return null;
|
|
60
|
+
if (ampm) {
|
|
61
|
+
if (hh < 1 || hh > 12)
|
|
62
|
+
return null;
|
|
63
|
+
if (ampm === 'AM')
|
|
64
|
+
hh = hh === 12 ? 0 : hh;
|
|
65
|
+
if (ampm === 'PM')
|
|
66
|
+
hh = hh === 12 ? 12 : hh + 12;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
if (hh < 0 || hh > 23)
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return hh * 60 + mm;
|
|
73
|
+
};
|
|
74
|
+
const getLocalTimeParts = (iso, tz) => {
|
|
75
|
+
const d = new Date(String(iso || ''));
|
|
76
|
+
if (Number.isNaN(d.getTime()))
|
|
77
|
+
return null;
|
|
78
|
+
const dtf = new Intl.DateTimeFormat('en-US', {
|
|
79
|
+
timeZone: tz,
|
|
80
|
+
hour: '2-digit',
|
|
81
|
+
minute: '2-digit',
|
|
82
|
+
hour12: false,
|
|
83
|
+
weekday: 'short',
|
|
84
|
+
day: '2-digit',
|
|
85
|
+
});
|
|
86
|
+
const parts = dtf.formatToParts(d);
|
|
87
|
+
let hourStr = '';
|
|
88
|
+
let minStr = '';
|
|
89
|
+
let weekdayStr = '';
|
|
90
|
+
let dayStr = '';
|
|
91
|
+
for (const p of parts) {
|
|
92
|
+
if (p.type === 'hour')
|
|
93
|
+
hourStr = p.value;
|
|
94
|
+
if (p.type === 'minute')
|
|
95
|
+
minStr = p.value;
|
|
96
|
+
if (p.type === 'weekday')
|
|
97
|
+
weekdayStr = p.value;
|
|
98
|
+
if (p.type === 'day')
|
|
99
|
+
dayStr = p.value;
|
|
100
|
+
}
|
|
101
|
+
const hour = Number(hourStr);
|
|
102
|
+
const minute = Number(minStr);
|
|
103
|
+
const dayOfMonth = Number(dayStr);
|
|
104
|
+
if (!Number.isFinite(hour) || !Number.isFinite(minute) || !Number.isFinite(dayOfMonth))
|
|
105
|
+
return null;
|
|
106
|
+
const minutes = hour * 60 + minute;
|
|
107
|
+
const weekdayMap = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
|
|
108
|
+
const weekday = weekdayMap[weekdayStr] ?? -1;
|
|
109
|
+
if (weekday < 0)
|
|
110
|
+
return null;
|
|
111
|
+
return { minutes, weekday, dayOfMonth };
|
|
112
|
+
};
|
|
113
|
+
const parseWeekdayToIndex = (raw) => {
|
|
114
|
+
const s = String(raw || '').trim().toLowerCase();
|
|
115
|
+
if (!s)
|
|
116
|
+
return null;
|
|
117
|
+
const map = {
|
|
118
|
+
sunday: 0, sun: 0,
|
|
119
|
+
monday: 1, mon: 1,
|
|
120
|
+
tuesday: 2, tue: 2, tues: 2,
|
|
121
|
+
wednesday: 3, wed: 3,
|
|
122
|
+
thursday: 4, thu: 4, thurs: 4,
|
|
123
|
+
friday: 5, fri: 5,
|
|
124
|
+
saturday: 6, sat: 6,
|
|
125
|
+
};
|
|
126
|
+
return map[s] ?? null;
|
|
127
|
+
};
|
|
128
|
+
const matchesInterest = (raw, op, expected) => {
|
|
129
|
+
if (raw === null || raw === undefined)
|
|
130
|
+
return false;
|
|
131
|
+
const a = String(raw).trim();
|
|
132
|
+
const b = String(expected ?? '').trim();
|
|
133
|
+
if (!b)
|
|
134
|
+
return false;
|
|
135
|
+
const nOp = String(op || 'equals').trim();
|
|
136
|
+
if (nOp === 'equals')
|
|
137
|
+
return a === b;
|
|
138
|
+
if (nOp === 'contains')
|
|
139
|
+
return a.toLowerCase().includes(b.toLowerCase());
|
|
140
|
+
if (nOp === 'starts_with')
|
|
141
|
+
return a.toLowerCase().startsWith(b.toLowerCase());
|
|
142
|
+
if (nOp === 'ends_with')
|
|
143
|
+
return a.toLowerCase().endsWith(b.toLowerCase());
|
|
144
|
+
if (nOp === 'not_equals')
|
|
145
|
+
return a !== b;
|
|
146
|
+
return a.toLowerCase().includes(b.toLowerCase());
|
|
147
|
+
};
|
|
148
|
+
const normalizeEventPropertyOp = (opRaw) => {
|
|
149
|
+
const op = String(opRaw ?? '').trim();
|
|
150
|
+
if (op === '=' || op === '==' || op === 'eq' || op === 'equals')
|
|
151
|
+
return 'equals';
|
|
152
|
+
if (op === '!=' || op === '<>' || op === 'neq' || op === 'not_equals')
|
|
153
|
+
return 'not_equals';
|
|
154
|
+
if (op === 'contains')
|
|
155
|
+
return 'contains';
|
|
156
|
+
if (op === 'not_contains')
|
|
157
|
+
return 'not_contains';
|
|
158
|
+
if (op === 'starts_with')
|
|
159
|
+
return 'starts_with';
|
|
160
|
+
if (op === 'ends_with')
|
|
161
|
+
return 'ends_with';
|
|
162
|
+
if (op === 'is_empty')
|
|
163
|
+
return 'is_empty';
|
|
164
|
+
if (op === 'is_not_empty')
|
|
165
|
+
return 'is_not_empty';
|
|
166
|
+
if (op === '>' || op === 'gt')
|
|
167
|
+
return 'gt';
|
|
168
|
+
if (op === '>=' || op === 'gte')
|
|
169
|
+
return 'gte';
|
|
170
|
+
if (op === '<' || op === 'lt')
|
|
171
|
+
return 'lt';
|
|
172
|
+
if (op === '<=' || op === 'lte')
|
|
173
|
+
return 'lte';
|
|
174
|
+
return op;
|
|
175
|
+
};
|
|
176
|
+
const matchesEventPropertyFilter = (raw, opRaw, expected) => {
|
|
177
|
+
const op = normalizeEventPropertyOp(opRaw);
|
|
178
|
+
if (op === 'is_empty') {
|
|
179
|
+
return raw === null || raw === undefined || String(raw).trim() === '';
|
|
180
|
+
}
|
|
181
|
+
if (op === 'is_not_empty') {
|
|
182
|
+
return raw !== null && raw !== undefined && String(raw).trim() !== '';
|
|
183
|
+
}
|
|
184
|
+
if (raw === null || raw === undefined)
|
|
185
|
+
return false;
|
|
186
|
+
const a = String(raw);
|
|
187
|
+
const b = String(expected ?? '');
|
|
188
|
+
if (op === 'equals')
|
|
189
|
+
return a === b;
|
|
190
|
+
if (op === 'not_equals')
|
|
191
|
+
return a !== b;
|
|
192
|
+
if (op === 'contains')
|
|
193
|
+
return a.toLowerCase().includes(b.toLowerCase());
|
|
194
|
+
if (op === 'not_contains')
|
|
195
|
+
return !a.toLowerCase().includes(b.toLowerCase());
|
|
196
|
+
if (op === 'starts_with')
|
|
197
|
+
return a.toLowerCase().startsWith(b.toLowerCase());
|
|
198
|
+
if (op === 'ends_with')
|
|
199
|
+
return a.toLowerCase().endsWith(b.toLowerCase());
|
|
200
|
+
if (op === 'gt' || op === 'gte' || op === 'lt' || op === 'lte') {
|
|
201
|
+
const aNum = Number(raw);
|
|
202
|
+
const bNum = Number(expected);
|
|
203
|
+
if (!Number.isFinite(aNum) || !Number.isFinite(bNum))
|
|
204
|
+
return false;
|
|
205
|
+
if (op === 'gt')
|
|
206
|
+
return aNum > bNum;
|
|
207
|
+
if (op === 'gte')
|
|
208
|
+
return aNum >= bNum;
|
|
209
|
+
if (op === 'lt')
|
|
210
|
+
return aNum < bNum;
|
|
211
|
+
return aNum <= bNum;
|
|
212
|
+
}
|
|
213
|
+
return false;
|
|
214
|
+
};
|
|
215
|
+
const applyEventRuleFilters = (rows, filters, opts) => {
|
|
216
|
+
let out = rows;
|
|
217
|
+
const tz = String(opts?.timezone || '').trim();
|
|
218
|
+
for (const f of filters) {
|
|
219
|
+
const type = String(f?.type || '').trim();
|
|
220
|
+
if (type === 'event_property') {
|
|
221
|
+
const key = String(f?.key || '').trim();
|
|
222
|
+
if (!key)
|
|
223
|
+
continue;
|
|
224
|
+
const op = f?.op;
|
|
225
|
+
const value = f?.value;
|
|
226
|
+
out = out.filter((row) => matchesEventPropertyFilter(getEventPropertyValue(row?.event_data, key), op, value));
|
|
227
|
+
}
|
|
228
|
+
else if (type === 'time_of_day') {
|
|
229
|
+
if (!tz)
|
|
230
|
+
continue;
|
|
231
|
+
const startMin = parseTimeToMinutes(f?.timeStart);
|
|
232
|
+
const endMin = parseTimeToMinutes(f?.timeEnd);
|
|
233
|
+
if (startMin == null || endMin == null)
|
|
234
|
+
continue;
|
|
235
|
+
if (startMin > endMin) {
|
|
236
|
+
out = [];
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
out = out.filter((row) => {
|
|
240
|
+
const parts = getLocalTimeParts(row?.event_timestamp, tz);
|
|
241
|
+
if (!parts)
|
|
242
|
+
return false;
|
|
243
|
+
return parts.minutes >= startMin && parts.minutes <= endMin;
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
else if (type === 'day_of_week') {
|
|
247
|
+
if (!tz)
|
|
248
|
+
continue;
|
|
249
|
+
const days = Array.isArray(f?.days) ? f.days : [];
|
|
250
|
+
const allowed = new Set();
|
|
251
|
+
for (const d of days) {
|
|
252
|
+
const idx = parseWeekdayToIndex(d);
|
|
253
|
+
if (idx != null)
|
|
254
|
+
allowed.add(idx);
|
|
255
|
+
}
|
|
256
|
+
if (allowed.size === 0)
|
|
257
|
+
continue;
|
|
258
|
+
out = out.filter((row) => {
|
|
259
|
+
const parts = getLocalTimeParts(row?.event_timestamp, tz);
|
|
260
|
+
if (!parts)
|
|
261
|
+
return false;
|
|
262
|
+
return allowed.has(parts.weekday);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
else if (type === 'day_of_month') {
|
|
266
|
+
if (!tz)
|
|
267
|
+
continue;
|
|
268
|
+
const days = Array.isArray(f?.days) ? f.days : [];
|
|
269
|
+
const allowed = new Set();
|
|
270
|
+
for (const d of days) {
|
|
271
|
+
const n = Number(d);
|
|
272
|
+
if (Number.isFinite(n) && n >= 1 && n <= 31)
|
|
273
|
+
allowed.add(Math.floor(n));
|
|
274
|
+
}
|
|
275
|
+
if (allowed.size === 0)
|
|
276
|
+
continue;
|
|
277
|
+
out = out.filter((row) => {
|
|
278
|
+
const parts = getLocalTimeParts(row?.event_timestamp, tz);
|
|
279
|
+
if (!parts)
|
|
280
|
+
return false;
|
|
281
|
+
return allowed.has(parts.dayOfMonth);
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return out;
|
|
286
|
+
};
|
|
5
287
|
class V2AudienceEngine {
|
|
6
288
|
constructor(params) {
|
|
7
289
|
this.supabase = params.supabaseClient;
|
|
@@ -10,6 +292,21 @@ class V2AudienceEngine {
|
|
|
10
292
|
}
|
|
11
293
|
async getContactIdsByAudienceCriteriaV2(organizationId, projectId, criteriaRaw) {
|
|
12
294
|
let criteria = CriteriaParser_1.CriteriaParser.parse(criteriaRaw);
|
|
295
|
+
let projectTimezone = 'UTC';
|
|
296
|
+
try {
|
|
297
|
+
const { data } = await this.supabase
|
|
298
|
+
.from('projects')
|
|
299
|
+
.select('settings')
|
|
300
|
+
.eq('organization_id', organizationId)
|
|
301
|
+
.eq('id', projectId)
|
|
302
|
+
.single();
|
|
303
|
+
const tzCandidate = data?.settings?.timezone;
|
|
304
|
+
if (typeof tzCandidate === 'string' && isValidIanaTimezone(tzCandidate)) {
|
|
305
|
+
projectTimezone = tzCandidate.trim();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
}
|
|
13
310
|
if ((!Array.isArray(criteria.groups) || criteria.groups.length === 0) &&
|
|
14
311
|
(Array.isArray(criteria.filters) || Array.isArray(criteria.conditions))) {
|
|
15
312
|
criteria = coerceCriteriaToGroups(criteria);
|
|
@@ -32,12 +329,15 @@ class V2AudienceEngine {
|
|
|
32
329
|
.eq('project_id', projectId);
|
|
33
330
|
const allContactIds = new Set((allContacts || []).map((c) => c.id));
|
|
34
331
|
const reachyIdToContactId = new Map();
|
|
332
|
+
const contactIdToReachyId = new Map();
|
|
35
333
|
const emailToContactId = new Map();
|
|
36
334
|
for (const c of allContacts || []) {
|
|
37
335
|
const rid = c?.reachy_id ? String(c.reachy_id).trim() : '';
|
|
38
336
|
const cid = c?.id ? String(c.id).trim() : '';
|
|
39
|
-
if (rid && cid)
|
|
337
|
+
if (rid && cid) {
|
|
40
338
|
reachyIdToContactId.set(rid, cid);
|
|
339
|
+
contactIdToReachyId.set(cid, rid);
|
|
340
|
+
}
|
|
41
341
|
const email = c?.email ? String(c.email).trim().toLowerCase() : '';
|
|
42
342
|
if (email && cid)
|
|
43
343
|
emailToContactId.set(email, cid);
|
|
@@ -190,8 +490,13 @@ class V2AudienceEngine {
|
|
|
190
490
|
}
|
|
191
491
|
}
|
|
192
492
|
}
|
|
493
|
+
const interest = rule?.interest;
|
|
193
494
|
try {
|
|
194
|
-
|
|
495
|
+
let attributes = Array.isArray(rule.attributes) ? rule.attributes : (rule.attributes ? [rule.attributes] : []);
|
|
496
|
+
const interestKey = interest?.key ? String(interest.key).trim() : '';
|
|
497
|
+
if (interestKey) {
|
|
498
|
+
attributes = attributes.filter((a) => String(a?.key || '').trim() !== interestKey);
|
|
499
|
+
}
|
|
195
500
|
const trackerEvents = new Set([
|
|
196
501
|
'click',
|
|
197
502
|
'page_view',
|
|
@@ -275,10 +580,229 @@ class V2AudienceEngine {
|
|
|
275
580
|
const { data, error } = await query;
|
|
276
581
|
if (error)
|
|
277
582
|
return new Set();
|
|
583
|
+
let rows = (data || []);
|
|
584
|
+
const ruleFiltersAll = Array.isArray(rule.filters) ? rule.filters : [];
|
|
585
|
+
const hasFirstTime = ruleFiltersAll.some((f) => String(f?.type || '').trim() === 'first_time');
|
|
586
|
+
const hasLastTime = ruleFiltersAll.some((f) => String(f?.type || '').trim() === 'last_time');
|
|
587
|
+
const ruleFilters = ruleFiltersAll.filter((f) => {
|
|
588
|
+
const t = String(f?.type || '').trim();
|
|
589
|
+
return t && t !== 'first_time' && t !== 'last_time';
|
|
590
|
+
});
|
|
591
|
+
if (ruleFilters.length > 0) {
|
|
592
|
+
rows = applyEventRuleFilters(rows, ruleFilters, { timezone: projectTimezone });
|
|
593
|
+
}
|
|
594
|
+
if ((hasFirstTime || hasLastTime) && rows.length > 0) {
|
|
595
|
+
const now = new Date();
|
|
596
|
+
let windowStartIso;
|
|
597
|
+
let windowEndIso;
|
|
598
|
+
if (rule.time) {
|
|
599
|
+
if (rule.time.unit && rule.time.value != null) {
|
|
600
|
+
const unit = String(rule.time.unit);
|
|
601
|
+
const valueNum = Number(rule.time.value);
|
|
602
|
+
const units = {
|
|
603
|
+
minutes: 60 * 1000,
|
|
604
|
+
hours: 60 * 60 * 1000,
|
|
605
|
+
days: 24 * 60 * 60 * 1000,
|
|
606
|
+
weeks: 7 * 24 * 60 * 60 * 1000,
|
|
607
|
+
months: 30 * 24 * 60 * 60 * 1000,
|
|
608
|
+
};
|
|
609
|
+
const ms = units[unit] ?? units['days'];
|
|
610
|
+
if (Number.isFinite(valueNum) && valueNum >= 0) {
|
|
611
|
+
windowStartIso = new Date(now.getTime() - valueNum * ms).toISOString();
|
|
612
|
+
windowEndIso = now.toISOString();
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
if (rule.time.from)
|
|
617
|
+
windowStartIso = String(rule.time.from);
|
|
618
|
+
if (rule.time.to)
|
|
619
|
+
windowEndIso = String(rule.time.to);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
else if (cfg && cfg.timeFrame) {
|
|
623
|
+
const tf = String(cfg.timeFrame).trim();
|
|
624
|
+
let unit = 'days';
|
|
625
|
+
let value = 7;
|
|
626
|
+
if (/^\d+$/.test(tf)) {
|
|
627
|
+
value = Number(tf);
|
|
628
|
+
unit = 'days';
|
|
629
|
+
}
|
|
630
|
+
else if (/^\d+\s*m$/.test(tf)) {
|
|
631
|
+
value = Number(tf.replace(/m$/, ''));
|
|
632
|
+
unit = 'minutes';
|
|
633
|
+
}
|
|
634
|
+
else if (/^\d+\s*h$/.test(tf)) {
|
|
635
|
+
value = Number(tf.replace(/h$/, ''));
|
|
636
|
+
unit = 'hours';
|
|
637
|
+
}
|
|
638
|
+
else if (/^\d+\s*d$/.test(tf)) {
|
|
639
|
+
value = Number(tf.replace(/d$/, ''));
|
|
640
|
+
unit = 'days';
|
|
641
|
+
}
|
|
642
|
+
else if (/^\d+\s*w$/.test(tf)) {
|
|
643
|
+
value = Number(tf.replace(/w$/, ''));
|
|
644
|
+
unit = 'weeks';
|
|
645
|
+
}
|
|
646
|
+
else if (/^\d+\s*mo$/.test(tf)) {
|
|
647
|
+
value = Number(tf.replace(/mo$/, ''));
|
|
648
|
+
unit = 'months';
|
|
649
|
+
}
|
|
650
|
+
const units = {
|
|
651
|
+
minutes: 60 * 1000,
|
|
652
|
+
hours: 60 * 60 * 1000,
|
|
653
|
+
days: 24 * 60 * 60 * 1000,
|
|
654
|
+
weeks: 7 * 24 * 60 * 60 * 1000,
|
|
655
|
+
months: 30 * 24 * 60 * 60 * 1000,
|
|
656
|
+
};
|
|
657
|
+
if (Number.isFinite(value) && value >= 0) {
|
|
658
|
+
const from = new Date(now.getTime() - value * units[unit]);
|
|
659
|
+
windowStartIso = from.toISOString();
|
|
660
|
+
windowEndIso = now.toISOString();
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
if (!windowEndIso)
|
|
664
|
+
windowEndIso = now.toISOString();
|
|
665
|
+
const candidateIds = new Set();
|
|
666
|
+
for (const row of rows) {
|
|
667
|
+
const id = resolveEventContactId(row);
|
|
668
|
+
if (id)
|
|
669
|
+
candidateIds.add(id);
|
|
670
|
+
}
|
|
671
|
+
if (candidateIds.size === 0) {
|
|
672
|
+
return new Set();
|
|
673
|
+
}
|
|
674
|
+
const candidateArr = Array.from(candidateIds);
|
|
675
|
+
const CHUNK = 500;
|
|
676
|
+
const PAGE = 1000;
|
|
677
|
+
const fetchOutside = async (dir, boundaryIso) => {
|
|
678
|
+
const outIds = new Set();
|
|
679
|
+
for (let i = 0; i < candidateArr.length; i += CHUNK) {
|
|
680
|
+
const chunkIds = candidateArr.slice(i, i + CHUNK);
|
|
681
|
+
const chunkReachyIds = chunkIds.map((cid) => contactIdToReachyId.get(cid)).filter(Boolean);
|
|
682
|
+
const runPaged = async (queryBuilderFactory) => {
|
|
683
|
+
let offset = 0;
|
|
684
|
+
while (true) {
|
|
685
|
+
const query = queryBuilderFactory(offset, offset + PAGE - 1);
|
|
686
|
+
const { data: rowsPage, error: pageError } = await query;
|
|
687
|
+
if (pageError || !rowsPage || rowsPage.length === 0)
|
|
688
|
+
break;
|
|
689
|
+
for (const r of rowsPage) {
|
|
690
|
+
const cid = resolveEventContactId(r);
|
|
691
|
+
if (cid)
|
|
692
|
+
outIds.add(cid);
|
|
693
|
+
}
|
|
694
|
+
if (outIds.size >= candidateIds.size)
|
|
695
|
+
return;
|
|
696
|
+
if (rowsPage.length < PAGE)
|
|
697
|
+
break;
|
|
698
|
+
offset += PAGE;
|
|
699
|
+
if (offset > 50000)
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
await runPaged((from, to) => {
|
|
704
|
+
let q = this.supabase
|
|
705
|
+
.from('contact_events')
|
|
706
|
+
.select('contact_id, reachy_id, event_timestamp')
|
|
707
|
+
.eq('organization_id', organizationId)
|
|
708
|
+
.eq('project_id', projectId)
|
|
709
|
+
.eq('event_name', effectiveEventName);
|
|
710
|
+
q = dir === 'before' ? q.lt('event_timestamp', boundaryIso) : q.gt('event_timestamp', boundaryIso);
|
|
711
|
+
q = q.in('contact_id', chunkIds);
|
|
712
|
+
return q.range(from, to);
|
|
713
|
+
});
|
|
714
|
+
if (outIds.size >= candidateIds.size)
|
|
715
|
+
break;
|
|
716
|
+
if (chunkReachyIds.length > 0) {
|
|
717
|
+
await runPaged((from, to) => {
|
|
718
|
+
let q = this.supabase
|
|
719
|
+
.from('contact_events')
|
|
720
|
+
.select('contact_id, reachy_id, event_timestamp')
|
|
721
|
+
.eq('organization_id', organizationId)
|
|
722
|
+
.eq('project_id', projectId)
|
|
723
|
+
.eq('event_name', effectiveEventName);
|
|
724
|
+
q = dir === 'before' ? q.lt('event_timestamp', boundaryIso) : q.gt('event_timestamp', boundaryIso);
|
|
725
|
+
q = q.in('reachy_id', chunkReachyIds);
|
|
726
|
+
return q.range(from, to);
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
if (outIds.size >= candidateIds.size)
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
return outIds;
|
|
733
|
+
};
|
|
734
|
+
let allowed = new Set(candidateIds);
|
|
735
|
+
if (hasFirstTime && windowStartIso) {
|
|
736
|
+
const hadBefore = await fetchOutside('before', windowStartIso);
|
|
737
|
+
allowed = diff(allowed, hadBefore);
|
|
738
|
+
}
|
|
739
|
+
if (hasLastTime && windowEndIso) {
|
|
740
|
+
const hadAfter = await fetchOutside('after', windowEndIso);
|
|
741
|
+
allowed = diff(allowed, hadAfter);
|
|
742
|
+
}
|
|
743
|
+
if (allowed.size === 0) {
|
|
744
|
+
return new Set();
|
|
745
|
+
}
|
|
746
|
+
rows = rows.filter((row) => {
|
|
747
|
+
const id = resolveEventContactId(row);
|
|
748
|
+
return id ? allowed.has(id) : false;
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
if (interest && interest.key) {
|
|
752
|
+
const key = String(interest.key || '').trim();
|
|
753
|
+
const op = String(interest.op || 'equals').trim();
|
|
754
|
+
const expected = interest.value;
|
|
755
|
+
const occType = String(interest.occurrenceType || 'predominantly').trim();
|
|
756
|
+
const pct = Number(interest.occurrencePercentage ?? 0);
|
|
757
|
+
const threshold = Number.isFinite(pct) ? Math.max(0, Math.min(100, pct)) / 100 : 0;
|
|
758
|
+
const totals = new Map();
|
|
759
|
+
const matchCounts = new Map();
|
|
760
|
+
const valueCounts = new Map();
|
|
761
|
+
for (const row of rows) {
|
|
762
|
+
const id = resolveEventContactId(row);
|
|
763
|
+
if (!id)
|
|
764
|
+
continue;
|
|
765
|
+
totals.set(id, (totals.get(id) || 0) + 1);
|
|
766
|
+
const rawVal = getEventPropertyValue(row?.event_data, key);
|
|
767
|
+
const strVal = rawVal === null || rawVal === undefined ? '' : String(rawVal);
|
|
768
|
+
if (!valueCounts.has(id))
|
|
769
|
+
valueCounts.set(id, new Map());
|
|
770
|
+
const per = valueCounts.get(id);
|
|
771
|
+
per.set(strVal, (per.get(strVal) || 0) + 1);
|
|
772
|
+
if (matchesInterest(rawVal, op === 'contains' ? 'contains' : op, expected)) {
|
|
773
|
+
matchCounts.set(id, (matchCounts.get(id) || 0) + 1);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
const res = new Set();
|
|
777
|
+
totals.forEach((total, id) => {
|
|
778
|
+
if (total <= 0)
|
|
779
|
+
return;
|
|
780
|
+
const match = matchCounts.get(id) || 0;
|
|
781
|
+
if (occType === 'at_least') {
|
|
782
|
+
if (match > 0 && match / total >= threshold)
|
|
783
|
+
res.add(id);
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
const per = valueCounts.get(id);
|
|
787
|
+
if (!per)
|
|
788
|
+
return;
|
|
789
|
+
let maxOther = 0;
|
|
790
|
+
per.forEach((cnt, valStr) => {
|
|
791
|
+
if (!matchesInterest(valStr, op, expected)) {
|
|
792
|
+
if (cnt > maxOther)
|
|
793
|
+
maxOther = cnt;
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
if (match > 0 && match >= maxOther)
|
|
797
|
+
res.add(id);
|
|
798
|
+
});
|
|
799
|
+
return res;
|
|
800
|
+
}
|
|
278
801
|
if (rule.frequency && rule.frequency.value != null) {
|
|
279
|
-
const { op, value, type = 'count', field = 'value' } = rule.frequency;
|
|
802
|
+
const { op, value, value2, type = 'count', field = 'value' } = rule.frequency;
|
|
280
803
|
const counts = new Map();
|
|
281
|
-
|
|
804
|
+
const denoms = new Map();
|
|
805
|
+
for (const row of rows) {
|
|
282
806
|
const id = resolveEventContactId(row);
|
|
283
807
|
if (!id)
|
|
284
808
|
continue;
|
|
@@ -286,16 +810,16 @@ class V2AudienceEngine {
|
|
|
286
810
|
counts.set(id, (counts.get(id) || 0) + 1);
|
|
287
811
|
}
|
|
288
812
|
else if (type === 'sum' || type === 'avg') {
|
|
289
|
-
let numVal = 0;
|
|
290
813
|
const eventData = row.event_data || {};
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
if (!Number.isNaN(numVal)) {
|
|
814
|
+
const raw = getEventPropertyValue(eventData, field) ??
|
|
815
|
+
(eventData?.[field] !== undefined ? eventData[field] : undefined) ??
|
|
816
|
+
eventData?.value ??
|
|
817
|
+
eventData?.amount;
|
|
818
|
+
const numVal = Number(raw);
|
|
819
|
+
if (Number.isFinite(numVal)) {
|
|
298
820
|
counts.set(id, (counts.get(id) || 0) + numVal);
|
|
821
|
+
if (type === 'avg')
|
|
822
|
+
denoms.set(id, (denoms.get(id) || 0) + 1);
|
|
299
823
|
}
|
|
300
824
|
}
|
|
301
825
|
}
|
|
@@ -303,20 +827,27 @@ class V2AudienceEngine {
|
|
|
303
827
|
counts.forEach((aggregatedValue, id) => {
|
|
304
828
|
let finalValue = aggregatedValue;
|
|
305
829
|
if (type === 'avg') {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const rid = resolveEventContactId(row);
|
|
309
|
-
if (rid === id)
|
|
310
|
-
eventCount++;
|
|
311
|
-
}
|
|
312
|
-
finalValue = eventCount > 0 ? aggregatedValue / eventCount : 0;
|
|
830
|
+
const denom = denoms.get(id) || 0;
|
|
831
|
+
finalValue = denom > 0 ? aggregatedValue / denom : 0;
|
|
313
832
|
}
|
|
833
|
+
const betweenOk = (() => {
|
|
834
|
+
if (op !== 'between')
|
|
835
|
+
return false;
|
|
836
|
+
const a = Number(value);
|
|
837
|
+
const b = Number(value2);
|
|
838
|
+
if (!Number.isFinite(a) || !Number.isFinite(b))
|
|
839
|
+
return false;
|
|
840
|
+
if (a > b)
|
|
841
|
+
return false;
|
|
842
|
+
return finalValue >= a && finalValue <= b;
|
|
843
|
+
})();
|
|
314
844
|
if ((op === '>=' && finalValue >= value) ||
|
|
315
845
|
(op === '>' && finalValue > value) ||
|
|
316
846
|
(op === '=' && finalValue === value) ||
|
|
317
847
|
(op === '!=' && finalValue !== value) ||
|
|
318
848
|
(op === '<=' && finalValue <= value) ||
|
|
319
|
-
(op === '<' && finalValue < value)
|
|
849
|
+
(op === '<' && finalValue < value) ||
|
|
850
|
+
betweenOk) {
|
|
320
851
|
res.add(id);
|
|
321
852
|
}
|
|
322
853
|
});
|
|
@@ -325,7 +856,7 @@ class V2AudienceEngine {
|
|
|
325
856
|
if (typeId === 'live-page-count' && cfg && cfg.pageCount != null) {
|
|
326
857
|
const threshold = Number(cfg.pageCount);
|
|
327
858
|
const counts = new Map();
|
|
328
|
-
for (const row of
|
|
859
|
+
for (const row of rows) {
|
|
329
860
|
const id = resolveEventContactId(row);
|
|
330
861
|
if (!id)
|
|
331
862
|
continue;
|
|
@@ -337,7 +868,7 @@ class V2AudienceEngine {
|
|
|
337
868
|
return res;
|
|
338
869
|
}
|
|
339
870
|
const res = new Set();
|
|
340
|
-
for (const row of
|
|
871
|
+
for (const row of rows) {
|
|
341
872
|
const id = resolveEventContactId(row);
|
|
342
873
|
if (id)
|
|
343
874
|
res.add(id);
|
|
@@ -606,6 +1137,21 @@ class V2AudienceEngine {
|
|
|
606
1137
|
return false;
|
|
607
1138
|
}
|
|
608
1139
|
const typeId = String(criteria?.type || '');
|
|
1140
|
+
let projectTimezone = 'UTC';
|
|
1141
|
+
try {
|
|
1142
|
+
const { data } = await this.supabase
|
|
1143
|
+
.from('projects')
|
|
1144
|
+
.select('settings')
|
|
1145
|
+
.eq('organization_id', organizationId)
|
|
1146
|
+
.eq('id', projectId)
|
|
1147
|
+
.single();
|
|
1148
|
+
const tzCandidate = data?.settings?.timezone;
|
|
1149
|
+
if (typeof tzCandidate === 'string' && isValidIanaTimezone(tzCandidate)) {
|
|
1150
|
+
projectTimezone = tzCandidate.trim();
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
catch {
|
|
1154
|
+
}
|
|
609
1155
|
const { data: contactRow } = await this.supabase
|
|
610
1156
|
.from('contacts')
|
|
611
1157
|
.select('id, reachy_id, email')
|
|
@@ -742,8 +1288,13 @@ class V2AudienceEngine {
|
|
|
742
1288
|
}
|
|
743
1289
|
}
|
|
744
1290
|
}
|
|
1291
|
+
const interest = rule?.interest;
|
|
745
1292
|
try {
|
|
746
|
-
|
|
1293
|
+
let attributes = Array.isArray(rule.attributes) ? rule.attributes : (rule.attributes ? [rule.attributes] : []);
|
|
1294
|
+
const interestKey = interest?.key ? String(interest.key).trim() : '';
|
|
1295
|
+
if (interestKey) {
|
|
1296
|
+
attributes = attributes.filter((a) => String(a?.key || '').trim() !== interestKey);
|
|
1297
|
+
}
|
|
747
1298
|
const resolveDbFieldForAttrKey = (keyRaw) => {
|
|
748
1299
|
const key = String(keyRaw || '').trim();
|
|
749
1300
|
if (!key)
|
|
@@ -814,28 +1365,192 @@ class V2AudienceEngine {
|
|
|
814
1365
|
const { data, error } = await query;
|
|
815
1366
|
if (error)
|
|
816
1367
|
return false;
|
|
817
|
-
|
|
1368
|
+
let rows = (data || []);
|
|
1369
|
+
const ruleFiltersAll = Array.isArray(rule.filters) ? rule.filters : [];
|
|
1370
|
+
const hasFirstTime = ruleFiltersAll.some((f) => String(f?.type || '').trim() === 'first_time');
|
|
1371
|
+
const hasLastTime = ruleFiltersAll.some((f) => String(f?.type || '').trim() === 'last_time');
|
|
1372
|
+
const ruleFilters = ruleFiltersAll.filter((f) => {
|
|
1373
|
+
const t = String(f?.type || '').trim();
|
|
1374
|
+
return t && t !== 'first_time' && t !== 'last_time';
|
|
1375
|
+
});
|
|
1376
|
+
if (ruleFilters.length > 0) {
|
|
1377
|
+
rows = applyEventRuleFilters(rows, ruleFilters, { timezone: projectTimezone });
|
|
1378
|
+
}
|
|
1379
|
+
if ((hasFirstTime || hasLastTime) && rows.length > 0) {
|
|
1380
|
+
const now = new Date();
|
|
1381
|
+
let windowStartIso;
|
|
1382
|
+
let windowEndIso;
|
|
1383
|
+
if (rule.time) {
|
|
1384
|
+
if (rule.time.unit && rule.time.value != null) {
|
|
1385
|
+
const unit = String(rule.time.unit);
|
|
1386
|
+
const valueNum = Number(rule.time.value);
|
|
1387
|
+
const units = {
|
|
1388
|
+
minutes: 60 * 1000,
|
|
1389
|
+
hours: 60 * 60 * 1000,
|
|
1390
|
+
days: 24 * 60 * 60 * 1000,
|
|
1391
|
+
weeks: 7 * 24 * 60 * 60 * 1000,
|
|
1392
|
+
months: 30 * 24 * 60 * 60 * 1000,
|
|
1393
|
+
};
|
|
1394
|
+
const ms = units[unit] ?? units['days'];
|
|
1395
|
+
if (Number.isFinite(valueNum) && valueNum >= 0) {
|
|
1396
|
+
windowStartIso = new Date(now.getTime() - valueNum * ms).toISOString();
|
|
1397
|
+
windowEndIso = now.toISOString();
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
else {
|
|
1401
|
+
if (rule.time.from)
|
|
1402
|
+
windowStartIso = String(rule.time.from);
|
|
1403
|
+
if (rule.time.to)
|
|
1404
|
+
windowEndIso = String(rule.time.to);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
else if (cfg && cfg.timeFrame) {
|
|
1408
|
+
const tf = String(cfg.timeFrame).trim();
|
|
1409
|
+
let unit = 'days';
|
|
1410
|
+
let value = 7;
|
|
1411
|
+
if (/^\d+$/.test(tf)) {
|
|
1412
|
+
value = Number(tf);
|
|
1413
|
+
unit = 'days';
|
|
1414
|
+
}
|
|
1415
|
+
else if (/^\d+\s*m$/.test(tf)) {
|
|
1416
|
+
value = Number(tf.replace(/m$/, ''));
|
|
1417
|
+
unit = 'minutes';
|
|
1418
|
+
}
|
|
1419
|
+
else if (/^\d+\s*h$/.test(tf)) {
|
|
1420
|
+
value = Number(tf.replace(/h$/, ''));
|
|
1421
|
+
unit = 'hours';
|
|
1422
|
+
}
|
|
1423
|
+
else if (/^\d+\s*d$/.test(tf)) {
|
|
1424
|
+
value = Number(tf.replace(/d$/, ''));
|
|
1425
|
+
unit = 'days';
|
|
1426
|
+
}
|
|
1427
|
+
else if (/^\d+\s*w$/.test(tf)) {
|
|
1428
|
+
value = Number(tf.replace(/w$/, ''));
|
|
1429
|
+
unit = 'weeks';
|
|
1430
|
+
}
|
|
1431
|
+
else if (/^\d+\s*mo$/.test(tf)) {
|
|
1432
|
+
value = Number(tf.replace(/mo$/, ''));
|
|
1433
|
+
unit = 'months';
|
|
1434
|
+
}
|
|
1435
|
+
const units = {
|
|
1436
|
+
minutes: 60 * 1000,
|
|
1437
|
+
hours: 60 * 60 * 1000,
|
|
1438
|
+
days: 24 * 60 * 60 * 1000,
|
|
1439
|
+
weeks: 7 * 24 * 60 * 60 * 1000,
|
|
1440
|
+
months: 30 * 24 * 60 * 60 * 1000,
|
|
1441
|
+
};
|
|
1442
|
+
if (Number.isFinite(value) && value >= 0) {
|
|
1443
|
+
const from = new Date(now.getTime() - value * units[unit]);
|
|
1444
|
+
windowStartIso = from.toISOString();
|
|
1445
|
+
windowEndIso = now.toISOString();
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
if (!windowEndIso)
|
|
1449
|
+
windowEndIso = now.toISOString();
|
|
1450
|
+
if (hasFirstTime && windowStartIso) {
|
|
1451
|
+
let q = this.supabase
|
|
1452
|
+
.from('contact_events')
|
|
1453
|
+
.select('id')
|
|
1454
|
+
.eq('organization_id', organizationId)
|
|
1455
|
+
.eq('project_id', projectId)
|
|
1456
|
+
.eq('event_name', effectiveEventName)
|
|
1457
|
+
.lt('event_timestamp', windowStartIso)
|
|
1458
|
+
.limit(1);
|
|
1459
|
+
const ors2 = [];
|
|
1460
|
+
if (contactId)
|
|
1461
|
+
ors2.push(`contact_id.eq.${contactId}`);
|
|
1462
|
+
if (reachyId)
|
|
1463
|
+
ors2.push(`reachy_id.eq.${reachyId}`);
|
|
1464
|
+
if (ors2.length > 0)
|
|
1465
|
+
q = q.or(ors2.join(','));
|
|
1466
|
+
const { data: beforeRows } = await q;
|
|
1467
|
+
if (Array.isArray(beforeRows) && beforeRows.length > 0)
|
|
1468
|
+
return false;
|
|
1469
|
+
}
|
|
1470
|
+
if (hasLastTime && windowEndIso) {
|
|
1471
|
+
let q = this.supabase
|
|
1472
|
+
.from('contact_events')
|
|
1473
|
+
.select('id')
|
|
1474
|
+
.eq('organization_id', organizationId)
|
|
1475
|
+
.eq('project_id', projectId)
|
|
1476
|
+
.eq('event_name', effectiveEventName)
|
|
1477
|
+
.gt('event_timestamp', windowEndIso)
|
|
1478
|
+
.limit(1);
|
|
1479
|
+
const ors2 = [];
|
|
1480
|
+
if (contactId)
|
|
1481
|
+
ors2.push(`contact_id.eq.${contactId}`);
|
|
1482
|
+
if (reachyId)
|
|
1483
|
+
ors2.push(`reachy_id.eq.${reachyId}`);
|
|
1484
|
+
if (ors2.length > 0)
|
|
1485
|
+
q = q.or(ors2.join(','));
|
|
1486
|
+
const { data: afterRows } = await q;
|
|
1487
|
+
if (Array.isArray(afterRows) && afterRows.length > 0)
|
|
1488
|
+
return false;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
if (interest && interest.key) {
|
|
1492
|
+
const key = String(interest.key || '').trim();
|
|
1493
|
+
const op = String(interest.op || 'equals').trim();
|
|
1494
|
+
const expected = interest.value;
|
|
1495
|
+
const occType = String(interest.occurrenceType || 'predominantly').trim();
|
|
1496
|
+
const pct = Number(interest.occurrencePercentage ?? 0);
|
|
1497
|
+
const threshold = Number.isFinite(pct) ? Math.max(0, Math.min(100, pct)) / 100 : 0;
|
|
1498
|
+
if (rows.length === 0)
|
|
1499
|
+
return false;
|
|
1500
|
+
let total = 0;
|
|
1501
|
+
let match = 0;
|
|
1502
|
+
const counts = new Map();
|
|
1503
|
+
for (const row of rows) {
|
|
1504
|
+
total++;
|
|
1505
|
+
const rawVal = getEventPropertyValue(row?.event_data, key);
|
|
1506
|
+
const strVal = rawVal === null || rawVal === undefined ? '' : String(rawVal);
|
|
1507
|
+
counts.set(strVal, (counts.get(strVal) || 0) + 1);
|
|
1508
|
+
if (matchesInterest(rawVal, op === 'contains' ? 'contains' : op, expected))
|
|
1509
|
+
match++;
|
|
1510
|
+
}
|
|
1511
|
+
if (occType === 'at_least') {
|
|
1512
|
+
return match > 0 && match / total >= threshold;
|
|
1513
|
+
}
|
|
1514
|
+
let maxOther = 0;
|
|
1515
|
+
counts.forEach((cnt, valStr) => {
|
|
1516
|
+
if (!matchesInterest(valStr, op, expected)) {
|
|
1517
|
+
if (cnt > maxOther)
|
|
1518
|
+
maxOther = cnt;
|
|
1519
|
+
}
|
|
1520
|
+
});
|
|
1521
|
+
return match > 0 && match >= maxOther;
|
|
1522
|
+
}
|
|
818
1523
|
if (rule.frequency && rule.frequency.value != null) {
|
|
819
|
-
const { op, value, type = 'count', field = 'value' } = rule.frequency;
|
|
1524
|
+
const { op, value, value2, type = 'count', field = 'value' } = rule.frequency;
|
|
820
1525
|
let aggregatedValue = 0;
|
|
1526
|
+
let denom = 0;
|
|
821
1527
|
if (type === 'count') {
|
|
822
1528
|
aggregatedValue = rows.length;
|
|
823
1529
|
}
|
|
824
1530
|
else {
|
|
825
1531
|
for (const row of rows) {
|
|
826
1532
|
const eventData = row.event_data || {};
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
numVal = Number(eventData['amount']);
|
|
834
|
-
if (!Number.isNaN(numVal))
|
|
1533
|
+
const raw = getEventPropertyValue(eventData, field) ??
|
|
1534
|
+
(eventData?.[field] !== undefined ? eventData[field] : undefined) ??
|
|
1535
|
+
eventData?.value ??
|
|
1536
|
+
eventData?.amount;
|
|
1537
|
+
const numVal = Number(raw);
|
|
1538
|
+
if (Number.isFinite(numVal)) {
|
|
835
1539
|
aggregatedValue += numVal;
|
|
1540
|
+
denom += 1;
|
|
1541
|
+
}
|
|
836
1542
|
}
|
|
837
1543
|
if (type === 'avg')
|
|
838
|
-
aggregatedValue =
|
|
1544
|
+
aggregatedValue = denom > 0 ? aggregatedValue / denom : 0;
|
|
1545
|
+
}
|
|
1546
|
+
if (op === 'between') {
|
|
1547
|
+
const a = Number(value);
|
|
1548
|
+
const b = Number(value2);
|
|
1549
|
+
if (!Number.isFinite(a) || !Number.isFinite(b))
|
|
1550
|
+
return false;
|
|
1551
|
+
if (a > b)
|
|
1552
|
+
return false;
|
|
1553
|
+
return aggregatedValue >= a && aggregatedValue <= b;
|
|
839
1554
|
}
|
|
840
1555
|
return ((op === '>=' && aggregatedValue >= value) ||
|
|
841
1556
|
(op === '>' && aggregatedValue > value) ||
|