@ht-sdks/events-sdk-js-browser 1.3.2 → 1.5.0

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.
Files changed (65) hide show
  1. package/dist/cjs/browser/index.js +4 -4
  2. package/dist/cjs/browser/index.js.map +1 -1
  3. package/dist/cjs/generated/version.js +1 -1
  4. package/dist/cjs/lib/tsub/index.js +13 -0
  5. package/dist/cjs/lib/tsub/index.js.map +1 -0
  6. package/dist/cjs/lib/tsub/matchers.js +416 -0
  7. package/dist/cjs/lib/tsub/matchers.js.map +1 -0
  8. package/dist/cjs/lib/tsub/store.js +23 -0
  9. package/dist/cjs/lib/tsub/store.js.map +1 -0
  10. package/dist/cjs/lib/tsub/transformers.js +218 -0
  11. package/dist/cjs/lib/tsub/transformers.js.map +1 -0
  12. package/dist/cjs/lib/tsub/unset.js +20 -0
  13. package/dist/cjs/lib/tsub/unset.js.map +1 -0
  14. package/dist/cjs/plugins/routing-middleware/index.js +1 -1
  15. package/dist/cjs/plugins/routing-middleware/index.js.map +1 -1
  16. package/dist/pkg/browser/index.js +4 -4
  17. package/dist/pkg/browser/index.js.map +1 -1
  18. package/dist/pkg/generated/version.js +1 -1
  19. package/dist/pkg/lib/tsub/index.js +4 -0
  20. package/dist/pkg/lib/tsub/index.js.map +1 -0
  21. package/dist/pkg/lib/tsub/matchers.js +412 -0
  22. package/dist/pkg/lib/tsub/matchers.js.map +1 -0
  23. package/dist/pkg/lib/tsub/store.js +21 -0
  24. package/dist/pkg/lib/tsub/store.js.map +1 -0
  25. package/dist/pkg/lib/tsub/transformers.js +214 -0
  26. package/dist/pkg/lib/tsub/transformers.js.map +1 -0
  27. package/dist/pkg/lib/tsub/unset.js +15 -0
  28. package/dist/pkg/lib/tsub/unset.js.map +1 -0
  29. package/dist/pkg/plugins/routing-middleware/index.js +1 -1
  30. package/dist/pkg/plugins/routing-middleware/index.js.map +1 -1
  31. package/dist/types/core/buffer/index.d.ts +1 -1
  32. package/dist/types/generated/version.d.ts +1 -1
  33. package/dist/types/lib/tsub/index.d.ts +4 -0
  34. package/dist/types/lib/tsub/index.d.ts.map +1 -0
  35. package/dist/types/lib/tsub/matchers.d.ts +5 -0
  36. package/dist/types/lib/tsub/matchers.d.ts.map +1 -0
  37. package/dist/types/lib/tsub/store.d.ts +22 -0
  38. package/dist/types/lib/tsub/store.d.ts.map +1 -0
  39. package/dist/types/lib/tsub/transformers.d.ts +20 -0
  40. package/dist/types/lib/tsub/transformers.d.ts.map +1 -0
  41. package/dist/types/lib/tsub/unset.d.ts +2 -0
  42. package/dist/types/lib/tsub/unset.d.ts.map +1 -0
  43. package/dist/types/plugins/routing-middleware/index.d.ts +3 -3
  44. package/dist/types/plugins/routing-middleware/index.d.ts.map +1 -1
  45. package/dist/umd/events.min.js +1 -1
  46. package/dist/umd/events.min.js.map +1 -1
  47. package/dist/umd/index.js +1 -1
  48. package/dist/umd/index.js.map +1 -1
  49. package/dist/umd/tsub-middleware.bundle.59e8d6916920ab24b51c.js +2 -0
  50. package/dist/umd/tsub-middleware.bundle.59e8d6916920ab24b51c.js.map +1 -0
  51. package/package.json +3 -2
  52. package/src/browser/index.ts +7 -7
  53. package/src/core/analytics/index.ts +1 -1
  54. package/src/generated/version.ts +1 -1
  55. package/src/lib/tsub/README.md +9 -0
  56. package/src/lib/tsub/index.ts +3 -0
  57. package/src/lib/tsub/matchers.ts +498 -0
  58. package/src/lib/tsub/store.ts +42 -0
  59. package/src/lib/tsub/transformers.ts +282 -0
  60. package/src/lib/tsub/unset.ts +14 -0
  61. package/src/plugins/routing-middleware/index.ts +3 -4
  62. package/dist/umd/870.bundle.6d7307379da86a3bf277.js +0 -2
  63. package/dist/umd/870.bundle.6d7307379da86a3bf277.js.map +0 -1
  64. package/dist/umd/tsub-middleware.bundle.a9604b3195f6189e429b.js +0 -2
  65. package/dist/umd/tsub-middleware.bundle.a9604b3195f6189e429b.js.map +0 -1
@@ -0,0 +1,498 @@
1
+ import * as Store from './store'
2
+ import get from 'dlv'
3
+
4
+ type Event = Record<string, any>
5
+
6
+ export default function matches(event: Event, matcher: Store.Matcher): boolean {
7
+ if (!matcher) {
8
+ throw new Error('No matcher supplied!')
9
+ }
10
+
11
+ switch (matcher.type) {
12
+ case 'all':
13
+ return all()
14
+ case 'fql':
15
+ return fql(matcher.ir, event)
16
+ default:
17
+ throw new Error(`Matcher of type ${matcher.type} unsupported.`)
18
+ }
19
+ }
20
+
21
+ function all(): boolean {
22
+ return true
23
+ }
24
+
25
+ function fql(ir: Store.Matcher['ir'], event: Event): boolean {
26
+ if (!ir) {
27
+ return false
28
+ }
29
+
30
+ try {
31
+ ir = JSON.parse(ir)
32
+ } catch (e) {
33
+ throw new Error(
34
+ `Failed to JSON.parse FQL intermediate representation "${ir}": ${e}`
35
+ )
36
+ }
37
+
38
+ const result = fqlEvaluate(ir, event)
39
+ if (typeof result !== 'boolean') {
40
+ // An error was returned, or a lowercase, typeof, or similar function was run alone. Nothing to evaluate.
41
+ return false
42
+ }
43
+
44
+ return result
45
+ }
46
+
47
+ // FQL is 100% type strict in Go. Show no mercy to types which do not comply.
48
+ function fqlEvaluate(ir: any, event: Event): any {
49
+ // If the given ir chunk is not an array, then we should check the single given path or value for literally `true`.
50
+ if (!Array.isArray(ir)) {
51
+ return getValue(ir, event) === true
52
+ }
53
+
54
+ // Otherwise, it is a sequence of ordered steps to follow to reach our solution!
55
+ const item = ir[0]
56
+ switch (item) {
57
+ /*** Unary cases ***/
58
+ // '!' => Invert the result
59
+ case '!':
60
+ return !fqlEvaluate(ir[1], event)
61
+
62
+ /*** Binary cases ***/
63
+ // 'or' => Any condition being true returns true
64
+ case 'or':
65
+ for (let i = 1; i < ir.length; i++) {
66
+ if (fqlEvaluate(ir[i], event)) {
67
+ return true
68
+ }
69
+ }
70
+ return false
71
+ // 'and' => Any condition being false returns false
72
+ case 'and':
73
+ for (let i = 1; i < ir.length; i++) {
74
+ if (!fqlEvaluate(ir[i], event)) {
75
+ return false
76
+ }
77
+ }
78
+ return true
79
+ // Equivalence comparisons
80
+ case '=':
81
+ case '!=':
82
+ return compareItems(
83
+ getValue(ir[1], event),
84
+ getValue(ir[2], event),
85
+ item,
86
+ event
87
+ )
88
+ // Numerical comparisons
89
+ case '<=':
90
+ case '<':
91
+ case '>':
92
+ case '>=':
93
+ // Compare the two values with the given operator.
94
+ return compareNumbers(
95
+ getValue(ir[1], event),
96
+ getValue(ir[2], event),
97
+ item,
98
+ event
99
+ )
100
+ // item in [list]' => Checks whether item is in list
101
+ case 'in':
102
+ return checkInList(getValue(ir[1], event), getValue(ir[2], event), event)
103
+
104
+ /*** Functions ***/
105
+ // 'contains(str1, str2)' => The first string has a substring of the second string
106
+ case 'contains':
107
+ return contains(getValue(ir[1], event), getValue(ir[2], event))
108
+ // 'match(str, match)' => The given string matches the provided glob matcher
109
+ case 'match':
110
+ return match(getValue(ir[1], event), getValue(ir[2], event))
111
+ // 'lowercase(str)' => Returns a lowercased string, null if the item is not a string
112
+ case 'lowercase': {
113
+ const target = getValue(ir[1], event)
114
+ if (typeof target !== 'string') {
115
+ return null
116
+ }
117
+ return target.toLowerCase()
118
+ }
119
+ // 'typeof(val)' => Returns the FQL type of the value
120
+ case 'typeof':
121
+ // TODO: Do we need mapping to allow for universal comparisons? e.g. Object -> JSON, Array -> List, Floats?
122
+ return typeof getValue(ir[1], event)
123
+ // 'length(val)' => Returns the length of an array or string, NaN if neither
124
+ case 'length':
125
+ return length(getValue(ir[1], event))
126
+ // If nothing hit, we or the IR messed up somewhere.
127
+ default:
128
+ throw new Error(`FQL IR could not evaluate for token: ${item}`)
129
+ }
130
+ }
131
+
132
+ function getValue(item: any, event: Event) {
133
+ // If item is an array, leave it as-is.
134
+ if (Array.isArray(item)) {
135
+ return item
136
+ }
137
+
138
+ // If item is an object, it has the form of `{"value": VAL}`
139
+ if (typeof item === 'object') {
140
+ return item.value
141
+ }
142
+
143
+ // Otherwise, it's an event path, e.g. "properties.email"
144
+ return get(event, item)
145
+ }
146
+
147
+ function checkInList(item: any, list: any[], event: Event): boolean {
148
+ return list.find((it) => getValue(it, event) === item) !== undefined
149
+ }
150
+
151
+ function compareNumbers(
152
+ first: any,
153
+ second: any,
154
+ operator: string,
155
+ event: Event
156
+ ): boolean {
157
+ // Check if it's more IR (such as a length() function)
158
+ if (isIR(first)) {
159
+ first = fqlEvaluate(first, event)
160
+ }
161
+
162
+ if (isIR(second)) {
163
+ second = fqlEvaluate(second, event)
164
+ }
165
+
166
+ if (typeof first !== 'number' || typeof second !== 'number') {
167
+ return false
168
+ }
169
+
170
+ // Reminder: NaN is not comparable to any other number (including NaN) and will always return false as desired.
171
+ switch (operator) {
172
+ // '<=' => The first number is less than or equal to the second.
173
+ case '<=':
174
+ return first <= second
175
+ // '>=' => The first number is greater than or equal to the second
176
+ case '>=':
177
+ return first >= second
178
+ // '<' The first number is less than the second.
179
+ case '<':
180
+ return first < second
181
+ // '>' The first number is greater than the second.
182
+ case '>':
183
+ return first > second
184
+ default:
185
+ throw new Error(`Invalid operator in compareNumbers: ${operator}`)
186
+ }
187
+ }
188
+
189
+ function compareItems(
190
+ first: any,
191
+ second: any,
192
+ operator: string,
193
+ event: Event
194
+ ): boolean {
195
+ // Check if it's more IR (such as a lowercase() function)
196
+ if (isIR(first)) {
197
+ first = fqlEvaluate(first, event)
198
+ }
199
+
200
+ if (isIR(second)) {
201
+ second = fqlEvaluate(second, event)
202
+ }
203
+
204
+ if (typeof first === 'object' && typeof second === 'object') {
205
+ first = JSON.stringify(first)
206
+ second = JSON.stringify(second)
207
+ }
208
+
209
+ // Objects with the exact same contents AND order ARE considered identical. (Don't compare by reference)
210
+ // Even in Go, this MUST be the same byte order.
211
+ // e.g. {a: 1, b:2} === {a: 1, b:2} BUT {a:1, b:2} !== {b:2, a:1}
212
+ // Maybe later we'll use a stable stringifier, but we're matching server-side behavior for now.
213
+ switch (operator) {
214
+ // '=' => The two following items are exactly identical
215
+ case '=':
216
+ return first === second
217
+ // '!=' => The two following items are NOT exactly identical.
218
+ case '!=':
219
+ return first !== second
220
+ default:
221
+ throw new Error(`Invalid operator in compareItems: ${operator}`)
222
+ }
223
+ }
224
+
225
+ function contains(first: any, second: any): boolean {
226
+ if (typeof first !== 'string' || typeof second !== 'string') {
227
+ return false
228
+ }
229
+
230
+ return first.indexOf(second) !== -1
231
+ }
232
+
233
+ function match(str: any, glob: any): boolean {
234
+ if (typeof str !== 'string' || typeof glob !== 'string') {
235
+ return false
236
+ }
237
+
238
+ return globMatches(glob, str)
239
+ }
240
+
241
+ function length(item: any) {
242
+ // Match server-side behavior.
243
+ if (item === null) {
244
+ return 0
245
+ }
246
+
247
+ // Type-check to avoid returning .length of an object
248
+ if (!Array.isArray(item) && typeof item !== 'string') {
249
+ return NaN
250
+ }
251
+
252
+ return item.length
253
+ }
254
+
255
+ // This is a heuristic technically speaking, but should be close enough. The odds of someone trying to test
256
+ // a func with identical IR notation is pretty low.
257
+ function isIR(value: any): boolean {
258
+ // TODO: This can be better checked by checking if this is a {"value": THIS}
259
+ if (!Array.isArray(value)) {
260
+ return false
261
+ }
262
+
263
+ // Function checks
264
+ if (
265
+ (value[0] === 'lowercase' ||
266
+ value[0] === 'length' ||
267
+ value[0] === 'typeof') &&
268
+ value.length === 2
269
+ ) {
270
+ return true
271
+ }
272
+
273
+ if ((value[0] === 'contains' || value[0] === 'match') && value.length === 3) {
274
+ return true
275
+ }
276
+
277
+ return false
278
+ }
279
+
280
+ // Any reputable glob matcher is designed to work on filesystems and doesn't allow the override of the separator
281
+ // character "/". This is problematic since our server-side representation e.g. evaluates "match('ab/c', 'a*)"
282
+ // as TRUE, whereas any glob matcher for JS available does false. So we're rewriting it here.
283
+ // See: https://github.com/segmentio/glob/blob/master/glob.go
284
+ function globMatches(pattern: string, str: string): boolean {
285
+ Pattern: while (pattern.length > 0) {
286
+ let star
287
+ let chunk
288
+ ;({ star, chunk, pattern } = scanChunk(pattern))
289
+ if (star && chunk === '') {
290
+ // Trailing * matches rest of string
291
+ return true
292
+ }
293
+
294
+ // Look for match at current position
295
+ let { t, ok, err } = matchChunk(chunk, str)
296
+ if (err) {
297
+ return false
298
+ }
299
+
300
+ // If we're the last chunk, make sure we've exhausted the str
301
+ // otherwise we'll give a false result even if we could still match
302
+ // using the star
303
+ if (ok && (t.length === 0 || pattern.length > 0)) {
304
+ str = t
305
+ continue
306
+ }
307
+
308
+ if (star) {
309
+ // Look for match, skipping i+1 bytes.
310
+ for (let i = 0; i < str.length; i++) {
311
+ ;({ t, ok, err } = matchChunk(chunk, str.slice(i + 1)))
312
+ if (ok) {
313
+ // If we're the last chunk, make sure we exhausted the str.
314
+ if (pattern.length === 0 && t.length > 0) {
315
+ continue
316
+ }
317
+
318
+ str = t
319
+ continue Pattern
320
+ }
321
+
322
+ if (err) {
323
+ return false
324
+ }
325
+ }
326
+ }
327
+
328
+ return false
329
+ }
330
+
331
+ return str.length === 0
332
+ }
333
+
334
+ function scanChunk(pattern: string): any {
335
+ const result = {
336
+ star: false,
337
+ chunk: '',
338
+ pattern: '',
339
+ }
340
+
341
+ while (pattern.length > 0 && pattern[0] === '*') {
342
+ pattern = pattern.slice(1)
343
+ result.star = true
344
+ }
345
+
346
+ let inRange = false
347
+ let i
348
+
349
+ Scan: for (i = 0; i < pattern.length; i++) {
350
+ switch (pattern[i]) {
351
+ case '\\':
352
+ // Error check handled in matchChunk: bad pattern.
353
+ if (i + 1 < pattern.length) {
354
+ i++
355
+ }
356
+ break
357
+ case '[':
358
+ inRange = true
359
+ break
360
+ case ']':
361
+ inRange = false
362
+ break
363
+ case '*':
364
+ if (!inRange) {
365
+ break Scan
366
+ }
367
+ }
368
+ }
369
+
370
+ result.chunk = pattern.slice(0, i)
371
+ result.pattern = pattern.slice(i)
372
+ return result
373
+ }
374
+
375
+ // matchChunk checks whether chunk matches the beginning of s.
376
+ // If so, it returns the remainder of s (after the match).
377
+ // Chunk is all single-character operators: literals, char classes, and ?.
378
+ function matchChunk(chunk: string, str: string): any {
379
+ const result = {
380
+ t: '',
381
+ ok: false,
382
+ err: false,
383
+ }
384
+
385
+ while (chunk.length > 0) {
386
+ if (str.length === 0) {
387
+ return result
388
+ }
389
+
390
+ switch (chunk[0]) {
391
+ case '[': {
392
+ const char = str[0]
393
+ str = str.slice(1)
394
+ chunk = chunk.slice(1)
395
+
396
+ let notNegated = true
397
+ if (chunk.length > 0 && chunk[0] === '^') {
398
+ notNegated = false
399
+ chunk = chunk.slice(1)
400
+ }
401
+
402
+ // Parse all ranges
403
+ let foundMatch = false
404
+ let nRange = 0
405
+
406
+ // eslint-disable-next-line no-constant-condition
407
+ while (true) {
408
+ if (chunk.length > 0 && chunk[0] === ']' && nRange > 0) {
409
+ chunk = chunk.slice(1)
410
+ break
411
+ }
412
+
413
+ let lo = ''
414
+ let hi = ''
415
+ let err
416
+ ;({ char: lo, newChunk: chunk, err } = getEsc(chunk))
417
+ if (err) {
418
+ return result
419
+ }
420
+
421
+ hi = lo
422
+ if (chunk[0] === '-') {
423
+ ;({ char: hi, newChunk: chunk, err } = getEsc(chunk.slice(1)))
424
+ if (err) {
425
+ return result
426
+ }
427
+ }
428
+
429
+ if (lo <= char && char <= hi) {
430
+ foundMatch = true
431
+ }
432
+
433
+ nRange++
434
+ }
435
+
436
+ if (foundMatch !== notNegated) {
437
+ return result
438
+ }
439
+
440
+ break
441
+ }
442
+ case '?':
443
+ str = str.slice(1)
444
+ chunk = chunk.slice(1)
445
+ break
446
+ case '\\':
447
+ chunk = chunk.slice(1)
448
+ if (chunk.length === 0) {
449
+ result.err = true
450
+ return result
451
+ }
452
+ // Fallthrough, missing break intentional.
453
+ default:
454
+ if (chunk[0] !== str[0]) {
455
+ return result
456
+ }
457
+ str = str.slice(1)
458
+ chunk = chunk.slice(1)
459
+ }
460
+ }
461
+
462
+ result.t = str
463
+ result.ok = true
464
+ result.err = false
465
+ return result
466
+ }
467
+
468
+ // getEsc gets a possibly-escaped character from chunk, for a character class.
469
+ function getEsc(chunk: string): any {
470
+ const result = {
471
+ char: '',
472
+ newChunk: '',
473
+ err: false,
474
+ }
475
+
476
+ if (chunk.length === 0 || chunk[0] === '-' || chunk[0] === ']') {
477
+ result.err = true
478
+ return result
479
+ }
480
+
481
+ if (chunk[0] === '\\') {
482
+ chunk = chunk.slice(1)
483
+ if (chunk.length === 0) {
484
+ result.err = true
485
+ return result
486
+ }
487
+ }
488
+
489
+ // Unlike Go, JS strings operate on characters instead of bytes.
490
+ // This is why we aren't copying over the GetRuneFromString stuff.
491
+ result.char = chunk[0]
492
+ result.newChunk = chunk.slice(1)
493
+ if (result.newChunk.length === 0) {
494
+ result.err = true
495
+ }
496
+
497
+ return result
498
+ }
@@ -0,0 +1,42 @@
1
+ import { TransformerConfig } from './transformers'
2
+
3
+ export interface Rule {
4
+ scope: string
5
+ target_type: string
6
+ matchers: Matcher[]
7
+ transformers: Transformer[][]
8
+ destinationName?: string
9
+ }
10
+
11
+ export interface Matcher {
12
+ type: string
13
+ ir: string
14
+ }
15
+
16
+ export interface Transformer {
17
+ type: string
18
+ config?: TransformerConfig | null
19
+ }
20
+
21
+ export default class Store {
22
+ private readonly rules: Rule[] = []
23
+
24
+ constructor(rules?: Rule[]) {
25
+ this.rules = rules || []
26
+ }
27
+
28
+ public getRulesByDestinationName(destinationName: string): Rule[] {
29
+ const rules: Rule[] = []
30
+ for (const rule of this.rules) {
31
+ // Rules with no destinationName are global (workspace || workspace::source)
32
+ if (
33
+ rule.destinationName === destinationName ||
34
+ rule.destinationName === undefined
35
+ ) {
36
+ rules.push(rule)
37
+ }
38
+ }
39
+
40
+ return rules
41
+ }
42
+ }