@posthog/core 1.1.0 → 1.2.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.
- package/dist/error-tracking/chunk-ids.js +1 -1
- package/dist/error-tracking/chunk-ids.mjs +1 -1
- package/dist/error-tracking/coercers/error-event-coercer.js +4 -5
- package/dist/error-tracking/coercers/error-event-coercer.mjs +4 -5
- package/dist/error-tracking/coercers/event-coercer.js +1 -2
- package/dist/error-tracking/coercers/event-coercer.mjs +1 -2
- package/dist/error-tracking/coercers/object-coercer.js +1 -2
- package/dist/error-tracking/coercers/object-coercer.mjs +1 -2
- package/dist/error-tracking/coercers/primitive-coercer.js +1 -2
- package/dist/error-tracking/coercers/primitive-coercer.mjs +1 -2
- package/dist/error-tracking/coercers/promise-rejection-event.js +4 -5
- package/dist/error-tracking/coercers/promise-rejection-event.mjs +4 -5
- package/dist/error-tracking/coercers/string-coercer.js +3 -4
- package/dist/error-tracking/coercers/string-coercer.mjs +3 -4
- package/dist/error-tracking/coercers/utils.js +2 -4
- package/dist/error-tracking/coercers/utils.mjs +2 -4
- package/dist/error-tracking/error-properties-builder.js +11 -15
- package/dist/error-tracking/error-properties-builder.mjs +11 -15
- package/dist/error-tracking/parsers/index.js +2 -4
- package/dist/error-tracking/parsers/index.mjs +2 -4
- package/dist/error-tracking/parsers/node.js +3 -5
- package/dist/error-tracking/parsers/node.mjs +3 -5
- package/dist/error-tracking/utils.js +4 -4
- package/dist/error-tracking/utils.mjs +4 -4
- package/dist/eventemitter.js +4 -4
- package/dist/eventemitter.mjs +4 -4
- package/dist/featureFlagUtils.js +20 -45
- package/dist/featureFlagUtils.mjs +20 -45
- package/dist/gzip.js +1 -2
- package/dist/gzip.mjs +1 -2
- package/dist/index.d.ts +4 -366
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +54 -1225
- package/dist/index.mjs +5 -1190
- package/dist/posthog-core-stateless.d.ts +204 -0
- package/dist/posthog-core-stateless.d.ts.map +1 -0
- package/dist/posthog-core-stateless.js +675 -0
- package/dist/posthog-core-stateless.mjs +632 -0
- package/dist/posthog-core.d.ts +171 -0
- package/dist/posthog-core.d.ts.map +1 -0
- package/dist/posthog-core.js +554 -0
- package/dist/posthog-core.mjs +520 -0
- package/dist/testing/PostHogCoreTestClient.d.ts +2 -1
- package/dist/testing/PostHogCoreTestClient.d.ts.map +1 -1
- package/dist/testing/PostHogCoreTestClient.js +9 -11
- package/dist/testing/PostHogCoreTestClient.mjs +8 -10
- package/dist/testing/test-utils.js +1 -1
- package/dist/testing/test-utils.mjs +1 -1
- package/dist/utils/bucketed-rate-limiter.js +8 -12
- package/dist/utils/bucketed-rate-limiter.mjs +8 -12
- package/dist/utils/index.js +3 -3
- package/dist/utils/index.mjs +3 -3
- package/dist/utils/type-utils.js +1 -1
- package/dist/utils/type-utils.mjs +1 -1
- package/dist/vendor/uuidv7.js +12 -16
- package/dist/vendor/uuidv7.mjs +12 -16
- package/package.json +3 -2
- package/src/__tests__/featureFlagUtils.spec.ts +427 -0
- package/src/__tests__/gzip.spec.ts +69 -0
- package/src/__tests__/posthog.ai.spec.ts +110 -0
- package/src/__tests__/posthog.capture.spec.ts +91 -0
- package/src/__tests__/posthog.core.spec.ts +135 -0
- package/src/__tests__/posthog.debug.spec.ts +36 -0
- package/src/__tests__/posthog.enqueue.spec.ts +93 -0
- package/src/__tests__/posthog.featureflags.spec.ts +1106 -0
- package/src/__tests__/posthog.featureflags.v1.spec.ts +922 -0
- package/src/__tests__/posthog.flush.spec.ts +237 -0
- package/src/__tests__/posthog.gdpr.spec.ts +50 -0
- package/src/__tests__/posthog.groups.spec.ts +96 -0
- package/src/__tests__/posthog.identify.spec.ts +194 -0
- package/src/__tests__/posthog.init.spec.ts +110 -0
- package/src/__tests__/posthog.listeners.spec.ts +51 -0
- package/src/__tests__/posthog.register.spec.ts +47 -0
- package/src/__tests__/posthog.reset.spec.ts +76 -0
- package/src/__tests__/posthog.sessions.spec.ts +63 -0
- package/src/__tests__/posthog.setProperties.spec.ts +102 -0
- package/src/__tests__/posthog.shutdown.spec.ts +88 -0
- package/src/__tests__/utils.spec.ts +36 -0
- package/src/error-tracking/chunk-ids.ts +58 -0
- package/src/error-tracking/coercers/dom-exception-coercer.ts +38 -0
- package/src/error-tracking/coercers/error-coercer.ts +36 -0
- package/src/error-tracking/coercers/error-event-coercer.ts +24 -0
- package/src/error-tracking/coercers/event-coercer.ts +19 -0
- package/src/error-tracking/coercers/index.ts +8 -0
- package/src/error-tracking/coercers/object-coercer.ts +76 -0
- package/src/error-tracking/coercers/primitive-coercer.ts +19 -0
- package/src/error-tracking/coercers/promise-rejection-event.spec.ts +77 -0
- package/src/error-tracking/coercers/promise-rejection-event.ts +53 -0
- package/src/error-tracking/coercers/string-coercer.spec.ts +26 -0
- package/src/error-tracking/coercers/string-coercer.ts +31 -0
- package/src/error-tracking/coercers/utils.ts +33 -0
- package/src/error-tracking/error-properties-builder.coerce.spec.ts +202 -0
- package/src/error-tracking/error-properties-builder.parse.spec.ts +30 -0
- package/src/error-tracking/error-properties-builder.ts +169 -0
- package/src/error-tracking/index.ts +5 -0
- package/src/error-tracking/parsers/base.ts +29 -0
- package/src/error-tracking/parsers/chrome.ts +53 -0
- package/src/error-tracking/parsers/gecko.ts +38 -0
- package/src/error-tracking/parsers/index.ts +104 -0
- package/src/error-tracking/parsers/node.ts +111 -0
- package/src/error-tracking/parsers/opera.ts +18 -0
- package/src/error-tracking/parsers/react-native.ts +0 -0
- package/src/error-tracking/parsers/safari.ts +33 -0
- package/src/error-tracking/parsers/winjs.ts +12 -0
- package/src/error-tracking/types.ts +107 -0
- package/src/error-tracking/utils.ts +39 -0
- package/src/eventemitter.ts +27 -0
- package/src/featureFlagUtils.ts +192 -0
- package/src/gzip.ts +29 -0
- package/src/index.ts +8 -0
- package/src/posthog-core-stateless.ts +1226 -0
- package/src/posthog-core.ts +958 -0
- package/src/testing/PostHogCoreTestClient.ts +91 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/test-utils.ts +47 -0
- package/src/types.ts +544 -0
- package/src/utils/bucketed-rate-limiter.spec.ts +33 -0
- package/src/utils/bucketed-rate-limiter.ts +85 -0
- package/src/utils/index.ts +98 -0
- package/src/utils/number-utils.spec.ts +89 -0
- package/src/utils/number-utils.ts +30 -0
- package/src/utils/promise-queue.spec.ts +55 -0
- package/src/utils/promise-queue.ts +30 -0
- package/src/utils/string-utils.ts +23 -0
- package/src/utils/type-utils.ts +134 -0
- package/src/vendor/uuidv7.ts +479 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Logger } from '@/types'
|
|
2
|
+
import { BucketedRateLimiter } from './bucketed-rate-limiter'
|
|
3
|
+
|
|
4
|
+
jest.useFakeTimers()
|
|
5
|
+
|
|
6
|
+
describe('BucketedRateLimiter', () => {
|
|
7
|
+
let rateLimiter: BucketedRateLimiter<string>
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
rateLimiter = new BucketedRateLimiter({
|
|
11
|
+
bucketSize: 10,
|
|
12
|
+
refillRate: 1,
|
|
13
|
+
refillInterval: 1000,
|
|
14
|
+
_logger: {} as unknown as Logger,
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
jest.clearAllMocks()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('it is not rate limited by default', () => {
|
|
23
|
+
const result = rateLimiter.consumeRateLimit('ResizeObserver')
|
|
24
|
+
expect(result).toBe(false)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('returns true if no mutations are left', () => {
|
|
28
|
+
rateLimiter['_buckets']['ResizeObserver'] = 0
|
|
29
|
+
|
|
30
|
+
const result = rateLimiter.consumeRateLimit('ResizeObserver')
|
|
31
|
+
expect(result).toBe(true)
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Logger } from '../types'
|
|
2
|
+
import { clampToRange } from './number-utils'
|
|
3
|
+
|
|
4
|
+
export class BucketedRateLimiter<T extends string | number> {
|
|
5
|
+
private _bucketSize
|
|
6
|
+
private _refillRate
|
|
7
|
+
private _refillInterval
|
|
8
|
+
private _onBucketRateLimited?: (key: T) => void
|
|
9
|
+
|
|
10
|
+
private _buckets: Record<string, number> = {}
|
|
11
|
+
private _removeInterval: NodeJS.Timeout | undefined
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
private readonly _options: {
|
|
15
|
+
bucketSize: number
|
|
16
|
+
refillRate: number
|
|
17
|
+
refillInterval: number
|
|
18
|
+
_logger: Logger
|
|
19
|
+
_onBucketRateLimited?: (key: T) => void
|
|
20
|
+
}
|
|
21
|
+
) {
|
|
22
|
+
this._onBucketRateLimited = this._options._onBucketRateLimited
|
|
23
|
+
this._bucketSize = clampToRange(this._options.bucketSize, 0, 100, this._options._logger)
|
|
24
|
+
this._refillRate = clampToRange(
|
|
25
|
+
this._options.refillRate,
|
|
26
|
+
0,
|
|
27
|
+
this._bucketSize, // never refill more than bucket size
|
|
28
|
+
this._options._logger
|
|
29
|
+
)
|
|
30
|
+
this._refillInterval = clampToRange(
|
|
31
|
+
this._options.refillInterval,
|
|
32
|
+
0,
|
|
33
|
+
86400000, // one day in milliseconds
|
|
34
|
+
this._options._logger
|
|
35
|
+
)
|
|
36
|
+
this._removeInterval = setInterval(() => {
|
|
37
|
+
this._refillBuckets()
|
|
38
|
+
}, this._refillInterval)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private _refillBuckets = () => {
|
|
42
|
+
Object.keys(this._buckets).forEach((key) => {
|
|
43
|
+
const newTokens = this._getBucket(key) + this._refillRate
|
|
44
|
+
|
|
45
|
+
if (newTokens >= this._bucketSize) {
|
|
46
|
+
delete this._buckets[key]
|
|
47
|
+
} else {
|
|
48
|
+
this._setBucket(key, newTokens)
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private _getBucket = (key: T | string) => {
|
|
54
|
+
return this._buckets[String(key)]
|
|
55
|
+
}
|
|
56
|
+
private _setBucket = (key: T | string, value: number) => {
|
|
57
|
+
this._buckets[String(key)] = value
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public consumeRateLimit = (key: T) => {
|
|
61
|
+
let tokens = this._getBucket(key) ?? this._bucketSize
|
|
62
|
+
tokens = Math.max(tokens - 1, 0)
|
|
63
|
+
|
|
64
|
+
if (tokens === 0) {
|
|
65
|
+
return true
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this._setBucket(key, tokens)
|
|
69
|
+
|
|
70
|
+
const hasReachedZero = tokens === 0
|
|
71
|
+
|
|
72
|
+
if (hasReachedZero) {
|
|
73
|
+
this._onBucketRateLimited?.(key)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return hasReachedZero
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public stop() {
|
|
80
|
+
if (this._removeInterval) {
|
|
81
|
+
clearInterval(this._removeInterval)
|
|
82
|
+
this._removeInterval = undefined
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { FetchLike } from '../types'
|
|
2
|
+
|
|
3
|
+
export * from './bucketed-rate-limiter'
|
|
4
|
+
export * from './number-utils'
|
|
5
|
+
export * from './string-utils'
|
|
6
|
+
export * from './type-utils'
|
|
7
|
+
export * from './promise-queue'
|
|
8
|
+
|
|
9
|
+
export const STRING_FORMAT = 'utf8'
|
|
10
|
+
|
|
11
|
+
export function assert(truthyValue: any, message: string): void {
|
|
12
|
+
if (!truthyValue || typeof truthyValue !== 'string' || isEmpty(truthyValue)) {
|
|
13
|
+
throw new Error(message)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isEmpty(truthyValue: string): boolean {
|
|
18
|
+
if (truthyValue.trim().length === 0) {
|
|
19
|
+
return true
|
|
20
|
+
}
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function removeTrailingSlash(url: string): string {
|
|
25
|
+
return url?.replace(/\/+$/, '')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface RetriableOptions {
|
|
29
|
+
retryCount: number
|
|
30
|
+
retryDelay: number
|
|
31
|
+
retryCheck: (err: unknown) => boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function retriable<T>(fn: () => Promise<T>, props: RetriableOptions): Promise<T> {
|
|
35
|
+
let lastError = null
|
|
36
|
+
|
|
37
|
+
for (let i = 0; i < props.retryCount + 1; i++) {
|
|
38
|
+
if (i > 0) {
|
|
39
|
+
// don't wait when it's the last try
|
|
40
|
+
await new Promise<void>((r) => setTimeout(r, props.retryDelay))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const res = await fn()
|
|
45
|
+
return res
|
|
46
|
+
} catch (e) {
|
|
47
|
+
lastError = e
|
|
48
|
+
if (!props.retryCheck(e)) {
|
|
49
|
+
throw e
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
throw lastError
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function currentTimestamp(): number {
|
|
58
|
+
return new Date().getTime()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function currentISOTime(): string {
|
|
62
|
+
return new Date().toISOString()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function safeSetTimeout(fn: () => void, timeout: number): any {
|
|
66
|
+
// NOTE: we use this so rarely that it is totally fine to do `safeSetTimeout(fn, 0)``
|
|
67
|
+
// rather than setImmediate.
|
|
68
|
+
const t = setTimeout(fn, timeout) as any
|
|
69
|
+
// We unref if available to prevent Node.js hanging on exit
|
|
70
|
+
t?.unref && t?.unref()
|
|
71
|
+
return t
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// NOTE: We opt for this slightly imperfect check as the global "Promise" object can get mutated in certain environments
|
|
75
|
+
export const isPromise = (obj: any): obj is Promise<any> => {
|
|
76
|
+
return obj && typeof obj.then === 'function'
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const isError = (x: unknown): x is Error => {
|
|
80
|
+
return x instanceof Error
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function getFetch(): FetchLike | undefined {
|
|
84
|
+
return typeof fetch !== 'undefined' ? fetch : typeof globalThis.fetch !== 'undefined' ? globalThis.fetch : undefined
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function allSettled<T>(
|
|
88
|
+
promises: (Promise<T> | null | undefined)[]
|
|
89
|
+
): Promise<({ status: 'fulfilled'; value: T } | { status: 'rejected'; reason: any })[]> {
|
|
90
|
+
return Promise.all(
|
|
91
|
+
promises.map((p) =>
|
|
92
|
+
(p ?? Promise.resolve()).then(
|
|
93
|
+
(value: any) => ({ status: 'fulfilled' as const, value }),
|
|
94
|
+
(reason: any) => ({ status: 'rejected' as const, reason })
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { createMockLogger } from '@/testing'
|
|
2
|
+
import { clampToRange } from './number-utils'
|
|
3
|
+
|
|
4
|
+
describe('number-utils', () => {
|
|
5
|
+
const mockLogger = createMockLogger()
|
|
6
|
+
|
|
7
|
+
describe('clampToRange', () => {
|
|
8
|
+
it.each([
|
|
9
|
+
[
|
|
10
|
+
'returns max when value is not a number',
|
|
11
|
+
{
|
|
12
|
+
value: null,
|
|
13
|
+
min: 10,
|
|
14
|
+
max: 100,
|
|
15
|
+
expected: 100,
|
|
16
|
+
fallback: undefined,
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
[
|
|
20
|
+
'returns max when value is not a number',
|
|
21
|
+
{
|
|
22
|
+
value: 'not-a-number',
|
|
23
|
+
min: 10,
|
|
24
|
+
max: 100,
|
|
25
|
+
expected: 100,
|
|
26
|
+
fallback: undefined,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
[
|
|
30
|
+
'returns max when value is greater than max',
|
|
31
|
+
{
|
|
32
|
+
value: 150,
|
|
33
|
+
min: 10,
|
|
34
|
+
max: 100,
|
|
35
|
+
expected: 100,
|
|
36
|
+
fallback: undefined,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
[
|
|
40
|
+
'returns min when value is less than min',
|
|
41
|
+
{
|
|
42
|
+
value: 5,
|
|
43
|
+
min: 10,
|
|
44
|
+
max: 100,
|
|
45
|
+
expected: 10,
|
|
46
|
+
fallback: undefined,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
[
|
|
50
|
+
'returns the value when it is within the range',
|
|
51
|
+
{
|
|
52
|
+
value: 50,
|
|
53
|
+
min: 10,
|
|
54
|
+
max: 100,
|
|
55
|
+
expected: 50,
|
|
56
|
+
fallback: undefined,
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
[
|
|
60
|
+
'returns the fallback value when provided is not valid',
|
|
61
|
+
{
|
|
62
|
+
value: 'invalid',
|
|
63
|
+
min: 10,
|
|
64
|
+
max: 100,
|
|
65
|
+
expected: 20,
|
|
66
|
+
fallback: 20,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
[
|
|
70
|
+
'returns the max value when fallback is not valid',
|
|
71
|
+
{
|
|
72
|
+
value: 'invalid',
|
|
73
|
+
min: 10,
|
|
74
|
+
max: 75,
|
|
75
|
+
expected: 75,
|
|
76
|
+
fallback: '20',
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
])('%s', (_description, { value, min, max, expected, fallback }) => {
|
|
80
|
+
const result = clampToRange(value, min, max, mockLogger, fallback as any)
|
|
81
|
+
expect(result).toBe(expected)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('logs a warning when min is greater than max', () => {
|
|
85
|
+
expect(clampToRange(50, 100, 10, mockLogger)).toBe(10)
|
|
86
|
+
expect(mockLogger.warn).toHaveBeenCalledWith('min cannot be greater than max.')
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Logger } from '../types'
|
|
2
|
+
import { isNumber } from './type-utils'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Clamps a value to a range.
|
|
6
|
+
* @param value the value to clamp
|
|
7
|
+
* @param min the minimum value
|
|
8
|
+
* @param max the maximum value
|
|
9
|
+
* @param label if provided then enables logging and prefixes all logs with labels
|
|
10
|
+
* @param fallbackValue if provided then returns this value if the value is not a valid number
|
|
11
|
+
*/
|
|
12
|
+
export function clampToRange(value: unknown, min: number, max: number, logger: Logger, fallbackValue?: number): number {
|
|
13
|
+
if (min > max) {
|
|
14
|
+
logger.warn('min cannot be greater than max.')
|
|
15
|
+
min = max
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!isNumber(value)) {
|
|
19
|
+
logger.warn(' must be a number. using max or fallback. max: ' + max + ', fallback: ' + fallbackValue)
|
|
20
|
+
return clampToRange(fallbackValue || max, min, max, logger)
|
|
21
|
+
} else if (value > max) {
|
|
22
|
+
logger.warn(' cannot be greater than max: ' + max + '. Using max value instead.')
|
|
23
|
+
return max
|
|
24
|
+
} else if (value < min) {
|
|
25
|
+
logger.warn(' cannot be less than min: ' + min + '. Using min value instead.')
|
|
26
|
+
return min
|
|
27
|
+
} else {
|
|
28
|
+
return value
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { PromiseQueue } from './promise-queue'
|
|
2
|
+
|
|
3
|
+
function buildPromise(time: number): Promise<number> {
|
|
4
|
+
return new Promise((res, rej) => setTimeout(() => res(42), time))
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function buildRecursivePromise(time: number, cb: () => void) {
|
|
8
|
+
return new Promise((res, rej) => {
|
|
9
|
+
setTimeout(() => {
|
|
10
|
+
cb()
|
|
11
|
+
res(42)
|
|
12
|
+
}, time)
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('promise-queue', () => {
|
|
17
|
+
beforeAll(() => jest.useRealTimers())
|
|
18
|
+
afterAll(() => jest.useFakeTimers())
|
|
19
|
+
|
|
20
|
+
it('should exit directly if the queue is empty', async () => {
|
|
21
|
+
const queue = new PromiseQueue()
|
|
22
|
+
expect(queue.length).toBe(0)
|
|
23
|
+
expect(queue.join()).resolves.toBe(undefined)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should add a promise to the queue', async () => {
|
|
27
|
+
const queue = new PromiseQueue()
|
|
28
|
+
queue.add(buildPromise(100))
|
|
29
|
+
expect(queue.length).toBe(1)
|
|
30
|
+
await queue.join()
|
|
31
|
+
expect(queue.length).toBe(0)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should wait even when promises create other promises', async () => {
|
|
35
|
+
const queue = new PromiseQueue()
|
|
36
|
+
const addSpy = jest.spyOn(queue, 'add')
|
|
37
|
+
queue.add(
|
|
38
|
+
buildRecursivePromise(100, () => {
|
|
39
|
+
queue.add(buildPromise(100))
|
|
40
|
+
})
|
|
41
|
+
)
|
|
42
|
+
expect(queue.length).toBe(1)
|
|
43
|
+
await queue.join()
|
|
44
|
+
expect(queue.length).toBe(0)
|
|
45
|
+
expect(addSpy).toHaveBeenCalledTimes(2)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('it should reject if a promise reject', async () => {
|
|
49
|
+
const queue = new PromiseQueue()
|
|
50
|
+
queue.add(Promise.reject(new Error('test')))
|
|
51
|
+
expect(queue.length).toBe(1)
|
|
52
|
+
await expect(queue.join()).rejects.toHaveProperty('message', 'test')
|
|
53
|
+
expect(queue.length).toBe(0)
|
|
54
|
+
})
|
|
55
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { uuidv7 } from '../vendor/uuidv7'
|
|
2
|
+
|
|
3
|
+
export class PromiseQueue {
|
|
4
|
+
private promiseByIds: Record<string, Promise<any>> = {}
|
|
5
|
+
|
|
6
|
+
public add(promise: Promise<any>): Promise<any> {
|
|
7
|
+
const promiseUUID = uuidv7()
|
|
8
|
+
this.promiseByIds[promiseUUID] = promise
|
|
9
|
+
promise
|
|
10
|
+
.catch(() => {})
|
|
11
|
+
.finally(() => {
|
|
12
|
+
delete this.promiseByIds[promiseUUID]
|
|
13
|
+
})
|
|
14
|
+
return promise
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public async join(): Promise<void> {
|
|
18
|
+
let promises = Object.values(this.promiseByIds)
|
|
19
|
+
let length = promises.length
|
|
20
|
+
while (length > 0) {
|
|
21
|
+
await Promise.all(promises)
|
|
22
|
+
promises = Object.values(this.promiseByIds)
|
|
23
|
+
length = promises.length
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public get length(): number {
|
|
28
|
+
return Object.keys(this.promiseByIds).length
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function includes(str: string, needle: string): boolean
|
|
2
|
+
export function includes<T>(arr: T[], needle: T): boolean
|
|
3
|
+
export function includes(str: unknown[] | string, needle: unknown): boolean {
|
|
4
|
+
return (str as any).indexOf(needle) !== -1
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const trim = function (str: string): string {
|
|
8
|
+
// Previous implementation was using underscore's trim function.
|
|
9
|
+
// When switching to just using the native trim() function, we ran some tests to make sure that it was able to trim both the BOM character \uFEFF and the NBSP character \u00A0.
|
|
10
|
+
// We tested modern Chrome (134.0.6998.118) and Firefox (136.0.2), and IE11 running on Windows 10, and all of them were able to trim both characters.
|
|
11
|
+
// See https://posthog.slack.com/archives/C0113360FFV/p1742811455647359
|
|
12
|
+
return str.trim()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// UNDERSCORE
|
|
16
|
+
// Embed part of the Underscore Library
|
|
17
|
+
export const stripLeadingDollar = function (s: string): string {
|
|
18
|
+
return s.replace(/^\$/, '')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isDistinctIdStringLike(value: string): boolean {
|
|
22
|
+
return ['distinct_id', 'distinctid'].includes(value.toLowerCase())
|
|
23
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { knownUnsafeEditableEvent, KnownUnsafeEditableEvent } from '../types'
|
|
2
|
+
import { includes } from './string-utils'
|
|
3
|
+
|
|
4
|
+
// eslint-disable-next-line posthog-js/no-direct-array-check
|
|
5
|
+
const nativeIsArray = Array.isArray
|
|
6
|
+
const ObjProto = Object.prototype
|
|
7
|
+
export const hasOwnProperty = ObjProto.hasOwnProperty
|
|
8
|
+
const toString = ObjProto.toString
|
|
9
|
+
|
|
10
|
+
export const isArray =
|
|
11
|
+
nativeIsArray ||
|
|
12
|
+
function (obj: any): obj is any[] {
|
|
13
|
+
return toString.call(obj) === '[object Array]'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// from a comment on http://dbj.org/dbj/?p=286
|
|
17
|
+
// fails on only one very rare and deliberate custom object:
|
|
18
|
+
// let bomb = { toString : undefined, valueOf: function(o) { return "function BOMBA!"; }};
|
|
19
|
+
export const isFunction = (x: unknown): x is (...args: any[]) => any => {
|
|
20
|
+
// eslint-disable-next-line posthog-js/no-direct-function-check
|
|
21
|
+
return typeof x === 'function'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const isNativeFunction = (x: unknown): x is (...args: any[]) => any =>
|
|
25
|
+
isFunction(x) && x.toString().indexOf('[native code]') !== -1
|
|
26
|
+
|
|
27
|
+
// Underscore Addons
|
|
28
|
+
export const isObject = (x: unknown): x is Record<string, any> => {
|
|
29
|
+
// eslint-disable-next-line posthog-js/no-direct-object-check
|
|
30
|
+
return x === Object(x) && !isArray(x)
|
|
31
|
+
}
|
|
32
|
+
export const isEmptyObject = (x: unknown) => {
|
|
33
|
+
if (isObject(x)) {
|
|
34
|
+
for (const key in x) {
|
|
35
|
+
if (hasOwnProperty.call(x, key)) {
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return true
|
|
40
|
+
}
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
export const isUndefined = (x: unknown): x is undefined => x === void 0
|
|
44
|
+
|
|
45
|
+
export const isString = (x: unknown): x is string => {
|
|
46
|
+
// eslint-disable-next-line posthog-js/no-direct-string-check
|
|
47
|
+
return toString.call(x) == '[object String]'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const isEmptyString = (x: unknown): boolean => isString(x) && x.trim().length === 0
|
|
51
|
+
|
|
52
|
+
export const isNull = (x: unknown): x is null => {
|
|
53
|
+
// eslint-disable-next-line posthog-js/no-direct-null-check
|
|
54
|
+
return x === null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/*
|
|
58
|
+
sometimes you want to check if something is null or undefined
|
|
59
|
+
that's what this is for
|
|
60
|
+
*/
|
|
61
|
+
export const isNullish = (x: unknown): x is null | undefined => isUndefined(x) || isNull(x)
|
|
62
|
+
|
|
63
|
+
export const isNumber = (x: unknown): x is number => {
|
|
64
|
+
// eslint-disable-next-line posthog-js/no-direct-number-check
|
|
65
|
+
return toString.call(x) == '[object Number]'
|
|
66
|
+
}
|
|
67
|
+
export const isBoolean = (x: unknown): x is boolean => {
|
|
68
|
+
// eslint-disable-next-line posthog-js/no-direct-boolean-check
|
|
69
|
+
return toString.call(x) === '[object Boolean]'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const isFormData = (x: unknown): x is FormData => {
|
|
73
|
+
// eslint-disable-next-line posthog-js/no-direct-form-data-check
|
|
74
|
+
return x instanceof FormData
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const isFile = (x: unknown): x is File => {
|
|
78
|
+
// eslint-disable-next-line posthog-js/no-direct-file-check
|
|
79
|
+
return x instanceof File
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const isPlainError = (x: unknown): x is Error => {
|
|
83
|
+
return x instanceof Error
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const isKnownUnsafeEditableEvent = (x: unknown): x is KnownUnsafeEditableEvent => {
|
|
87
|
+
return includes(knownUnsafeEditableEvent as unknown as string[], x)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function isInstanceOf(candidate: unknown, base: any): boolean {
|
|
91
|
+
try {
|
|
92
|
+
return candidate instanceof base
|
|
93
|
+
} catch {
|
|
94
|
+
return false
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function isPrimitive(value: unknown): boolean {
|
|
99
|
+
return value === null || typeof value !== 'object'
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function isBuiltin(candidate: unknown, className: string): boolean {
|
|
103
|
+
return Object.prototype.toString.call(candidate) === `[object ${className}]`
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function isError(candidate: unknown): candidate is Error {
|
|
107
|
+
switch (Object.prototype.toString.call(candidate)) {
|
|
108
|
+
case '[object Error]':
|
|
109
|
+
case '[object Exception]':
|
|
110
|
+
case '[object DOMException]':
|
|
111
|
+
case '[object DOMError]':
|
|
112
|
+
case '[object WebAssembly.Exception]':
|
|
113
|
+
return true
|
|
114
|
+
default:
|
|
115
|
+
return isInstanceOf(candidate, Error)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function isErrorEvent(event: unknown): boolean {
|
|
120
|
+
return isBuiltin(event, 'ErrorEvent')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function isEvent(candidate: unknown): candidate is Event {
|
|
124
|
+
return !isUndefined(Event) && isInstanceOf(candidate, Event)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function isPlainObject(candidate: unknown): candidate is Record<string, unknown> {
|
|
128
|
+
return isBuiltin(candidate, 'Object')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export const yesLikeValues = [true, 'true', 1, '1', 'yes']
|
|
132
|
+
export const isYesLike = (val: string | boolean | number): boolean => includes(yesLikeValues, val)
|
|
133
|
+
export const noLikeValues = [false, 'false', 0, '0', 'no']
|
|
134
|
+
export const isNoLike = (val: string | boolean | number): boolean => includes(noLikeValues, val)
|