@zuplo/cli 7.4.5 → 7.4.7
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/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/types/jwks/remote.d.ts +4 -4
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/types/jws/general/verify.d.ts +3 -3
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/types/jwt/encrypt.d.ts +8 -14
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/types/jwt/sign.d.ts +8 -14
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/types/jwt/unsecured.d.ts +9 -15
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/jwe/compact/encrypt.js +30 -8
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/jwe/flattened/decrypt.js +8 -4
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/jwe/flattened/encrypt.js +6 -13
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/jwe/general/decrypt.js +19 -19
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/jwe/general/encrypt.js +27 -20
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/jwk/embedded.js +15 -1
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/jwk/thumbprint.js +11 -5
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/jwks/local.js +45 -53
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/jwks/remote.js +82 -103
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/jws/compact/sign.js +10 -9
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/jws/flattened/sign.js +2 -1
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/jws/flattened/verify.js +8 -7
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/jws/general/sign.js +5 -3
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/jws/general/verify.js +51 -21
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/jwt/decrypt.js +7 -9
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/jwt/encrypt.js +21 -50
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/jwt/sign.js +8 -41
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/jwt/unsecured.js +21 -42
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/key/generate_key_pair.js +6 -2
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/key/generate_secret.js +7 -6
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/key/import.js +16 -12
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/lib/asn1.js +7 -2
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/lib/content_encryption.js +7 -15
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/lib/deflate.js +8 -0
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/lib/helpers.js +1 -1
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/lib/jwe_decrypt.js +85 -28
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/lib/jwe_encrypt.js +40 -12
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/lib/jwk_metadata.js +20 -0
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/lib/jws_sign.js +47 -36
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/lib/jws_verify.js +77 -45
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/lib/jwt_claims_set.js +82 -47
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/lib/key.js +24 -9
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/lib/key_management.js +10 -4
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/lib/key_options.js +6 -0
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/lib/options.js +26 -0
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/dist/webapi/lib/type_checks.js +11 -13
- package/node_modules/@modelcontextprotocol/client/node_modules/jose/package.json +1 -1
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/types/jwks/remote.d.ts +4 -4
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/types/jws/general/verify.d.ts +3 -3
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/types/jwt/encrypt.d.ts +8 -14
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/types/jwt/sign.d.ts +8 -14
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/types/jwt/unsecured.d.ts +9 -15
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/jwe/compact/encrypt.js +30 -8
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/jwe/flattened/decrypt.js +8 -4
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/jwe/flattened/encrypt.js +6 -13
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/jwe/general/decrypt.js +19 -19
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/jwe/general/encrypt.js +27 -20
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/jwk/embedded.js +15 -1
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/jwk/thumbprint.js +11 -5
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/jwks/local.js +45 -53
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/jwks/remote.js +82 -103
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/jws/compact/sign.js +10 -9
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/jws/flattened/sign.js +2 -1
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/jws/flattened/verify.js +8 -7
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/jws/general/sign.js +5 -3
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/jws/general/verify.js +51 -21
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/jwt/decrypt.js +7 -9
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/jwt/encrypt.js +21 -50
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/jwt/sign.js +8 -41
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/jwt/unsecured.js +21 -42
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/key/generate_key_pair.js +6 -2
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/key/generate_secret.js +7 -6
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/key/import.js +16 -12
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/lib/asn1.js +7 -2
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/lib/content_encryption.js +7 -15
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/lib/deflate.js +8 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/lib/helpers.js +1 -1
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/lib/jwe_decrypt.js +85 -28
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/lib/jwe_encrypt.js +40 -12
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/lib/jwk_metadata.js +20 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/lib/jws_sign.js +47 -36
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/lib/jws_verify.js +77 -45
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/lib/jwt_claims_set.js +82 -47
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/lib/key.js +24 -9
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/lib/key_management.js +10 -4
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/lib/key_options.js +6 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/lib/options.js +26 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/dist/webapi/lib/type_checks.js +11 -13
- package/node_modules/@modelcontextprotocol/sdk/node_modules/jose/package.json +1 -1
- package/node_modules/@posthog/core/dist/featureFlagLocalEvaluation.d.ts +35 -0
- package/node_modules/@posthog/core/dist/featureFlagLocalEvaluation.d.ts.map +1 -0
- package/node_modules/@posthog/core/dist/featureFlagLocalEvaluation.js +352 -0
- package/node_modules/@posthog/core/dist/featureFlagLocalEvaluation.mjs +294 -0
- package/node_modules/@posthog/core/dist/index.d.ts +2 -0
- package/node_modules/@posthog/core/dist/index.d.ts.map +1 -1
- package/node_modules/@posthog/core/dist/index.js +164 -79
- package/node_modules/@posthog/core/dist/index.mjs +2 -1
- package/node_modules/@posthog/core/dist/posthog-core-stateless.d.ts +12 -18
- package/node_modules/@posthog/core/dist/posthog-core-stateless.d.ts.map +1 -1
- package/node_modules/@posthog/core/dist/posthog-core-stateless.js +15 -47
- package/node_modules/@posthog/core/dist/posthog-core-stateless.mjs +15 -47
- package/node_modules/@posthog/core/dist/surveys/index.d.ts +1 -0
- package/node_modules/@posthog/core/dist/surveys/index.d.ts.map +1 -1
- package/node_modules/@posthog/core/dist/surveys/index.js +13 -0
- package/node_modules/@posthog/core/dist/surveys/index.mjs +2 -1
- package/node_modules/@posthog/core/dist/surveys/property-matching.d.ts +11 -0
- package/node_modules/@posthog/core/dist/surveys/property-matching.d.ts.map +1 -0
- package/node_modules/@posthog/core/dist/surveys/property-matching.js +89 -0
- package/node_modules/@posthog/core/dist/surveys/property-matching.mjs +46 -0
- package/node_modules/@posthog/core/dist/types.d.ts +15 -4
- package/node_modules/@posthog/core/dist/types.d.ts.map +1 -1
- package/node_modules/@posthog/core/package.json +2 -2
- package/node_modules/@posthog/core/src/featureFlagLocalEvaluation.ts +375 -0
- package/node_modules/@posthog/core/src/index.ts +19 -0
- package/node_modules/@posthog/core/src/logs/index.spec.ts +77 -0
- package/node_modules/@posthog/core/src/logs/index.ts +1 -1
- package/node_modules/@posthog/core/src/posthog-core-stateless.ts +40 -65
- package/node_modules/@posthog/core/src/surveys/index.ts +1 -0
- package/node_modules/@posthog/core/src/surveys/property-matching.spec.ts +174 -0
- package/node_modules/@posthog/core/src/surveys/property-matching.ts +75 -0
- package/node_modules/@posthog/core/src/types.ts +20 -4
- package/node_modules/@posthog/types/dist/index.d.ts +1 -1
- package/node_modules/@posthog/types/dist/index.d.ts.map +1 -1
- package/node_modules/@posthog/types/dist/posthog-config.d.ts +20 -5
- package/node_modules/@posthog/types/dist/posthog-config.d.ts.map +1 -1
- package/node_modules/@posthog/types/package.json +1 -1
- package/node_modules/@posthog/types/src/index.ts +1 -0
- package/node_modules/@posthog/types/src/posthog-config.ts +21 -5
- package/node_modules/@zuplo/core/customer.cli.minified.js +269 -269
- package/node_modules/@zuplo/core/index.minified.js +276 -276
- package/node_modules/@zuplo/core/package.json +1 -1
- package/node_modules/@zuplo/graphql/package.json +1 -1
- package/node_modules/@zuplo/openapi-tools/package.json +1 -1
- package/node_modules/@zuplo/otel/package.json +1 -1
- package/node_modules/@zuplo/runtime/out/esm/{chunk-IZE65N7D.js → chunk-CUK3VROF.js} +2 -2
- package/node_modules/@zuplo/runtime/out/esm/{chunk-4CDML74A.js → chunk-K4TBJG47.js} +2 -2
- package/node_modules/@zuplo/runtime/out/esm/{chunk-4VE55TG3.js → chunk-S4EBLGSU.js} +2 -2
- package/node_modules/@zuplo/runtime/out/esm/{chunk-4POCC75H.js → chunk-XF3IIUDX.js} +4 -4
- package/node_modules/@zuplo/runtime/out/esm/{chunk-4POCC75H.js.map → chunk-XF3IIUDX.js.map} +1 -1
- package/node_modules/@zuplo/runtime/out/esm/chunk-XIFGUD6K.js +403 -0
- package/node_modules/@zuplo/runtime/out/esm/chunk-XIFGUD6K.js.map +1 -0
- package/node_modules/@zuplo/runtime/out/esm/index.js +1 -1
- package/node_modules/@zuplo/runtime/out/esm/internal/index.js +1 -1
- package/node_modules/@zuplo/runtime/out/esm/mcp-gateway/index.js +1 -1
- package/node_modules/@zuplo/runtime/out/esm/mocks/index.js +1 -1
- package/node_modules/@zuplo/runtime/out/esm/sdks/aws/index.js +1 -1
- package/node_modules/@zuplo/runtime/package.json +1 -1
- package/node_modules/fast-uri/index.js +201 -48
- package/node_modules/fast-uri/lib/schemes.js +9 -4
- package/node_modules/fast-uri/lib/utils.js +388 -91
- package/node_modules/fast-uri/package.json +1 -1
- package/node_modules/fast-uri/test/component-safe-serialization.test.js +105 -0
- package/node_modules/fast-uri/test/equal.test.js +31 -3
- package/node_modules/fast-uri/test/fixtures/uri-js-parse.json +2 -0
- package/node_modules/fast-uri/test/ipv6-canonical.test.js +34 -0
- package/node_modules/fast-uri/test/ipv6-validation.test.js +90 -0
- package/node_modules/fast-uri/test/malformed-percent.test.js +77 -0
- package/node_modules/fast-uri/test/malformed-urn.test.js +61 -0
- package/node_modules/fast-uri/test/parse.test.js +7 -3
- package/node_modules/fast-uri/test/query-fragment-normalization.test.js +33 -0
- package/node_modules/fast-uri/test/reserved-path-normalization.test.js +109 -0
- package/node_modules/fast-uri/test/scheme-validation.test.js +124 -0
- package/node_modules/fast-uri/test/security-normalization.test.js +101 -0
- package/node_modules/fast-uri/test/security.test.js +75 -3
- package/node_modules/fast-uri/test/urn-full-input.test.js +29 -0
- package/node_modules/fast-uri/test/websocket-query-preservation.test.js +24 -0
- package/node_modules/hono/dist/cjs/client/client.js +25 -11
- package/node_modules/hono/dist/cjs/client/utils.js +3 -0
- package/node_modules/hono/dist/cjs/helper/accepts/accepts.js +36 -2
- package/node_modules/hono/dist/cjs/jsx/dom/render.js +2 -0
- package/node_modules/hono/dist/cjs/middleware/etag/index.js +1 -1
- package/node_modules/hono/dist/cjs/request.js +8 -4
- package/node_modules/hono/dist/cjs/router/reg-exp-router/node.js +10 -3
- package/node_modules/hono/dist/cjs/router/reg-exp-router/router.js +47 -83
- package/node_modules/hono/dist/cjs/router/reg-exp-router/trie.js +2 -1
- package/node_modules/hono/dist/cjs/router/trie-router/node.js +4 -3
- package/node_modules/hono/dist/cjs/router/utils.js +27 -0
- package/node_modules/hono/dist/cjs/utils/cookie.js +1 -1
- package/node_modules/hono/dist/cjs/utils/stream.js +7 -1
- package/node_modules/hono/dist/client/client.js +25 -11
- package/node_modules/hono/dist/client/utils.js +3 -0
- package/node_modules/hono/dist/helper/accepts/accepts.js +36 -2
- package/node_modules/hono/dist/jsx/dom/render.js +2 -0
- package/node_modules/hono/dist/middleware/etag/index.js +1 -1
- package/node_modules/hono/dist/request.js +8 -4
- package/node_modules/hono/dist/router/reg-exp-router/node.js +6 -2
- package/node_modules/hono/dist/router/reg-exp-router/router.js +53 -84
- package/node_modules/hono/dist/router/reg-exp-router/trie.js +2 -1
- package/node_modules/hono/dist/router/trie-router/node.js +4 -3
- package/node_modules/hono/dist/router/utils.js +5 -0
- package/node_modules/hono/dist/types/router/reg-exp-router/node.d.ts +3 -0
- package/node_modules/hono/dist/types/router/utils.d.ts +1 -0
- package/node_modules/hono/dist/utils/cookie.js +1 -1
- package/node_modules/hono/dist/utils/stream.js +7 -1
- package/node_modules/hono/package.json +1 -1
- package/package.json +11 -11
- package/node_modules/@zuplo/runtime/out/esm/chunk-4SX4HBWT.js +0 -409
- package/node_modules/@zuplo/runtime/out/esm/chunk-4SX4HBWT.js.map +0 -1
- /package/node_modules/@zuplo/runtime/out/esm/{chunk-IZE65N7D.js.map → chunk-CUK3VROF.js.map} +0 -0
- /package/node_modules/@zuplo/runtime/out/esm/{chunk-4CDML74A.js.map → chunk-K4TBJG47.js.map} +0 -0
- /package/node_modules/@zuplo/runtime/out/esm/{chunk-4VE55TG3.js.map → chunk-S4EBLGSU.js.map} +0 -0
- /package/node_modules/@zuplo/runtime/out/esm/{chunk-4POCC75H.js.LEGAL.txt → chunk-XF3IIUDX.js.LEGAL.txt} +0 -0
- /package/node_modules/@zuplo/runtime/out/esm/{chunk-4SX4HBWT.js.LEGAL.txt → chunk-XIFGUD6K.js.LEGAL.txt} +0 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { parsePayload } from './featureFlagUtils'
|
|
2
|
+
import type { FeatureFlagValue, JsonType } from './types'
|
|
3
|
+
|
|
4
|
+
export type FeatureFlagPropertyValue = string | number | (string | number)[] | boolean
|
|
5
|
+
|
|
6
|
+
export type FeatureFlagProperty = {
|
|
7
|
+
key: string
|
|
8
|
+
value: FeatureFlagPropertyValue
|
|
9
|
+
operator?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type FeatureFlagSemverParsingPolicy = 'strict' | 'legacy-permissive'
|
|
13
|
+
|
|
14
|
+
export type MatchFeatureFlagPropertyOptions = {
|
|
15
|
+
warnFunction?: (message: string) => void
|
|
16
|
+
semverParsingPolicy?: FeatureFlagSemverParsingPolicy
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type FeatureFlagVariant = {
|
|
20
|
+
key: string
|
|
21
|
+
rollout_percentage: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type FeatureFlagVariantLookupEntry = {
|
|
25
|
+
valueMin: number
|
|
26
|
+
valueMax: number
|
|
27
|
+
key: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const NULL_VALUES_ALLOWED_OPERATORS = ['is_not', 'is_set']
|
|
31
|
+
|
|
32
|
+
// This value is intentionally larger than Number.MAX_SAFE_INTEGER. Changing its rounding changes
|
|
33
|
+
// existing rollout and variant assignments.
|
|
34
|
+
// eslint-disable-next-line no-loss-of-precision
|
|
35
|
+
const LONG_SCALE = 0xfffffffffffffff
|
|
36
|
+
|
|
37
|
+
export class InconclusiveMatchError extends Error {
|
|
38
|
+
constructor(message: string) {
|
|
39
|
+
super(message)
|
|
40
|
+
this.name = this.constructor.name
|
|
41
|
+
Object.setPrototypeOf(this, InconclusiveMatchError.prototype)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isValidRegex(regex: string): boolean {
|
|
46
|
+
try {
|
|
47
|
+
new RegExp(regex)
|
|
48
|
+
return true
|
|
49
|
+
} catch {
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type SemverTuple = [number, number, number]
|
|
55
|
+
|
|
56
|
+
function parseSemverNumericIdentifier(
|
|
57
|
+
part: string,
|
|
58
|
+
raw: string,
|
|
59
|
+
parsingPolicy: FeatureFlagSemverParsingPolicy
|
|
60
|
+
): number {
|
|
61
|
+
if (!/^\d+$/.test(part) || (parsingPolicy === 'strict' && part.length > 1 && part[0] === '0')) {
|
|
62
|
+
throw new InconclusiveMatchError(`Invalid semver: ${raw}`)
|
|
63
|
+
}
|
|
64
|
+
return parseInt(part, 10)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function parseFeatureFlagSemver(
|
|
68
|
+
value: string,
|
|
69
|
+
parsingPolicy: FeatureFlagSemverParsingPolicy = 'strict'
|
|
70
|
+
): SemverTuple {
|
|
71
|
+
const text = String(value).trim().replace(/^[vV]/, '')
|
|
72
|
+
const baseVersion = text.split('-')[0].split('+')[0]
|
|
73
|
+
|
|
74
|
+
if (!baseVersion || baseVersion.startsWith('.')) {
|
|
75
|
+
throw new InconclusiveMatchError(`Invalid semver: ${value}`)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const parts = baseVersion.split('.')
|
|
79
|
+
const parsePart = (part: string | undefined): number => {
|
|
80
|
+
if (part === undefined || part === '') return 0
|
|
81
|
+
return parseSemverNumericIdentifier(part, value, parsingPolicy)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return [parsePart(parts[0]), parsePart(parts[1]), parsePart(parts[2])]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function compareSemverTuples(a: SemverTuple, b: SemverTuple): number {
|
|
88
|
+
for (let i = 0; i < 3; i++) {
|
|
89
|
+
if (a[i] < b[i]) return -1
|
|
90
|
+
if (a[i] > b[i]) return 1
|
|
91
|
+
}
|
|
92
|
+
return 0
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function computeTildeBounds(
|
|
96
|
+
value: string,
|
|
97
|
+
parsingPolicy: FeatureFlagSemverParsingPolicy
|
|
98
|
+
): { lower: SemverTuple; upper: SemverTuple } {
|
|
99
|
+
const parsed = parseFeatureFlagSemver(value, parsingPolicy)
|
|
100
|
+
return { lower: [parsed[0], parsed[1], parsed[2]], upper: [parsed[0], parsed[1] + 1, 0] }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function computeCaretBounds(
|
|
104
|
+
value: string,
|
|
105
|
+
parsingPolicy: FeatureFlagSemverParsingPolicy
|
|
106
|
+
): { lower: SemverTuple; upper: SemverTuple } {
|
|
107
|
+
const [major, minor, patch] = parseFeatureFlagSemver(value, parsingPolicy)
|
|
108
|
+
const lower: SemverTuple = [major, minor, patch]
|
|
109
|
+
let upper: SemverTuple
|
|
110
|
+
if (major > 0) upper = [major + 1, 0, 0]
|
|
111
|
+
else if (minor > 0) upper = [0, minor + 1, 0]
|
|
112
|
+
else upper = [0, 0, patch + 1]
|
|
113
|
+
return { lower, upper }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function computeWildcardBounds(
|
|
117
|
+
value: string,
|
|
118
|
+
parsingPolicy: FeatureFlagSemverParsingPolicy
|
|
119
|
+
): { lower: SemverTuple; upper: SemverTuple } {
|
|
120
|
+
const text = String(value).trim().replace(/^[vV]/, '')
|
|
121
|
+
const cleanedText = text.replace(/\.\*$/, '').replace(/\*$/, '')
|
|
122
|
+
if (!cleanedText) throw new InconclusiveMatchError(`Invalid wildcard semver: ${value}`)
|
|
123
|
+
|
|
124
|
+
const parts = cleanedText.split('.')
|
|
125
|
+
const parseWildcardPart = (part: string): number => {
|
|
126
|
+
if (parsingPolicy === 'legacy-permissive') {
|
|
127
|
+
const parsed = parseInt(part, 10)
|
|
128
|
+
if (!isNaN(parsed)) return parsed
|
|
129
|
+
} else {
|
|
130
|
+
try {
|
|
131
|
+
return parseSemverNumericIdentifier(part, value, parsingPolicy)
|
|
132
|
+
} catch {
|
|
133
|
+
// Normalize wildcard parsing failures to the historical wildcard-specific error.
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
throw new InconclusiveMatchError(`Invalid wildcard semver: ${value}`)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const major = parseWildcardPart(parts[0])
|
|
140
|
+
if (parts.length === 1) {
|
|
141
|
+
return { lower: [major, 0, 0], upper: [major + 1, 0, 0] }
|
|
142
|
+
}
|
|
143
|
+
const minor = parseWildcardPart(parts[1])
|
|
144
|
+
return { lower: [major, minor, 0], upper: [major, minor + 1, 0] }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function convertToDateTime(value: FeatureFlagPropertyValue | Date): Date {
|
|
148
|
+
if (value instanceof Date) return value
|
|
149
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
150
|
+
const date = new Date(value)
|
|
151
|
+
if (!isNaN(date.valueOf())) return date
|
|
152
|
+
throw new InconclusiveMatchError(`${value} is in an invalid date format`)
|
|
153
|
+
}
|
|
154
|
+
throw new InconclusiveMatchError(`The date provided ${value} must be a string, number, or date object`)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function relativeDateParseForFeatureFlagMatching(value: string): Date | null {
|
|
158
|
+
const regex = /^-?(?<number>[0-9]+)(?<interval>[a-z])$/
|
|
159
|
+
const match = value.match(regex)
|
|
160
|
+
const parsedDt = new Date(new Date().toISOString())
|
|
161
|
+
|
|
162
|
+
if (!match || !match.groups) return null
|
|
163
|
+
|
|
164
|
+
const number = parseInt(match.groups['number'])
|
|
165
|
+
if (number >= 10000) return null
|
|
166
|
+
|
|
167
|
+
const interval = match.groups['interval']
|
|
168
|
+
if (interval === 'h') parsedDt.setUTCHours(parsedDt.getUTCHours() - number)
|
|
169
|
+
else if (interval === 'd') parsedDt.setUTCDate(parsedDt.getUTCDate() - number)
|
|
170
|
+
else if (interval === 'w') parsedDt.setUTCDate(parsedDt.getUTCDate() - number * 7)
|
|
171
|
+
else if (interval === 'm') parsedDt.setUTCMonth(parsedDt.getUTCMonth() - number)
|
|
172
|
+
else if (interval === 'y') parsedDt.setUTCFullYear(parsedDt.getUTCFullYear() - number)
|
|
173
|
+
else return null
|
|
174
|
+
|
|
175
|
+
return parsedDt
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function matchFeatureFlagProperty(
|
|
179
|
+
property: FeatureFlagProperty,
|
|
180
|
+
propertyValues: Record<string, any>,
|
|
181
|
+
options: MatchFeatureFlagPropertyOptions = {}
|
|
182
|
+
): boolean {
|
|
183
|
+
const key = property.key
|
|
184
|
+
const value = property.value
|
|
185
|
+
const operator = property.operator || 'exact'
|
|
186
|
+
const parsingPolicy = options.semverParsingPolicy ?? 'strict'
|
|
187
|
+
|
|
188
|
+
if (!(key in propertyValues)) {
|
|
189
|
+
if (operator === 'is_not_set') return true
|
|
190
|
+
throw new InconclusiveMatchError(`Property ${key} not found in propertyValues`)
|
|
191
|
+
} else if (operator === 'is_not_set') {
|
|
192
|
+
return false
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const overrideValue = propertyValues[key]
|
|
196
|
+
if (overrideValue == null && !NULL_VALUES_ALLOWED_OPERATORS.includes(operator)) {
|
|
197
|
+
options.warnFunction?.(`Property ${key} cannot have a value of null/undefined with the ${operator} operator`)
|
|
198
|
+
return false
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const computeExactMatch = (target: any, actual: any): boolean => {
|
|
202
|
+
if (Array.isArray(target)) {
|
|
203
|
+
return target.map((item) => String(item).toLowerCase()).includes(String(actual).toLowerCase())
|
|
204
|
+
}
|
|
205
|
+
return String(target).toLowerCase() === String(actual).toLowerCase()
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const compare = (lhs: any, rhs: any, comparisonOperator: string): boolean => {
|
|
209
|
+
if (comparisonOperator === 'gt') return lhs > rhs
|
|
210
|
+
if (comparisonOperator === 'gte') return lhs >= rhs
|
|
211
|
+
if (comparisonOperator === 'lt') return lhs < rhs
|
|
212
|
+
if (comparisonOperator === 'lte') return lhs <= rhs
|
|
213
|
+
throw new Error(`Invalid operator: ${comparisonOperator}`)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
switch (operator) {
|
|
217
|
+
case 'exact':
|
|
218
|
+
return computeExactMatch(value, overrideValue)
|
|
219
|
+
case 'is_not':
|
|
220
|
+
return !computeExactMatch(value, overrideValue)
|
|
221
|
+
case 'is_set':
|
|
222
|
+
return key in propertyValues
|
|
223
|
+
case 'icontains':
|
|
224
|
+
return String(overrideValue).toLowerCase().includes(String(value).toLowerCase())
|
|
225
|
+
case 'not_icontains':
|
|
226
|
+
return !String(overrideValue).toLowerCase().includes(String(value).toLowerCase())
|
|
227
|
+
case 'starts_with':
|
|
228
|
+
return String(overrideValue).toLowerCase().startsWith(String(value).toLowerCase())
|
|
229
|
+
case 'not_starts_with':
|
|
230
|
+
return !String(overrideValue).toLowerCase().startsWith(String(value).toLowerCase())
|
|
231
|
+
case 'ends_with':
|
|
232
|
+
return String(overrideValue).toLowerCase().endsWith(String(value).toLowerCase())
|
|
233
|
+
case 'not_ends_with':
|
|
234
|
+
return !String(overrideValue).toLowerCase().endsWith(String(value).toLowerCase())
|
|
235
|
+
case 'regex':
|
|
236
|
+
return isValidRegex(String(value)) && String(overrideValue).match(String(value)) !== null
|
|
237
|
+
case 'not_regex':
|
|
238
|
+
return isValidRegex(String(value)) && String(overrideValue).match(String(value)) === null
|
|
239
|
+
case 'gt':
|
|
240
|
+
case 'gte':
|
|
241
|
+
case 'lt':
|
|
242
|
+
case 'lte': {
|
|
243
|
+
const parsedValue = typeof value === 'number' ? value : parseFloat(String(value))
|
|
244
|
+
const parsedOverride =
|
|
245
|
+
typeof overrideValue === 'number'
|
|
246
|
+
? overrideValue
|
|
247
|
+
: overrideValue != null
|
|
248
|
+
? parseFloat(String(overrideValue))
|
|
249
|
+
: NaN
|
|
250
|
+
if (Number.isFinite(parsedValue) && Number.isFinite(parsedOverride)) {
|
|
251
|
+
return compare(parsedOverride, parsedValue, operator)
|
|
252
|
+
}
|
|
253
|
+
return compare(String(overrideValue), String(value), operator)
|
|
254
|
+
}
|
|
255
|
+
case 'is_date_after':
|
|
256
|
+
case 'is_date_before': {
|
|
257
|
+
if (typeof value === 'boolean') {
|
|
258
|
+
throw new InconclusiveMatchError('Date operations cannot be performed on boolean values')
|
|
259
|
+
}
|
|
260
|
+
let parsedDate = relativeDateParseForFeatureFlagMatching(String(value))
|
|
261
|
+
if (parsedDate == null) parsedDate = convertToDateTime(value)
|
|
262
|
+
const overrideDate = convertToDateTime(overrideValue)
|
|
263
|
+
return operator === 'is_date_before' ? overrideDate < parsedDate : overrideDate > parsedDate
|
|
264
|
+
}
|
|
265
|
+
case 'semver_eq':
|
|
266
|
+
return (
|
|
267
|
+
compareSemverTuples(
|
|
268
|
+
parseFeatureFlagSemver(String(overrideValue), parsingPolicy),
|
|
269
|
+
parseFeatureFlagSemver(String(value), parsingPolicy)
|
|
270
|
+
) === 0
|
|
271
|
+
)
|
|
272
|
+
case 'semver_neq':
|
|
273
|
+
return (
|
|
274
|
+
compareSemverTuples(
|
|
275
|
+
parseFeatureFlagSemver(String(overrideValue), parsingPolicy),
|
|
276
|
+
parseFeatureFlagSemver(String(value), parsingPolicy)
|
|
277
|
+
) !== 0
|
|
278
|
+
)
|
|
279
|
+
case 'semver_gt':
|
|
280
|
+
return (
|
|
281
|
+
compareSemverTuples(
|
|
282
|
+
parseFeatureFlagSemver(String(overrideValue), parsingPolicy),
|
|
283
|
+
parseFeatureFlagSemver(String(value), parsingPolicy)
|
|
284
|
+
) > 0
|
|
285
|
+
)
|
|
286
|
+
case 'semver_gte':
|
|
287
|
+
return (
|
|
288
|
+
compareSemverTuples(
|
|
289
|
+
parseFeatureFlagSemver(String(overrideValue), parsingPolicy),
|
|
290
|
+
parseFeatureFlagSemver(String(value), parsingPolicy)
|
|
291
|
+
) >= 0
|
|
292
|
+
)
|
|
293
|
+
case 'semver_lt':
|
|
294
|
+
return (
|
|
295
|
+
compareSemverTuples(
|
|
296
|
+
parseFeatureFlagSemver(String(overrideValue), parsingPolicy),
|
|
297
|
+
parseFeatureFlagSemver(String(value), parsingPolicy)
|
|
298
|
+
) < 0
|
|
299
|
+
)
|
|
300
|
+
case 'semver_lte':
|
|
301
|
+
return (
|
|
302
|
+
compareSemverTuples(
|
|
303
|
+
parseFeatureFlagSemver(String(overrideValue), parsingPolicy),
|
|
304
|
+
parseFeatureFlagSemver(String(value), parsingPolicy)
|
|
305
|
+
) <= 0
|
|
306
|
+
)
|
|
307
|
+
case 'semver_tilde': {
|
|
308
|
+
const overrideParsed = parseFeatureFlagSemver(String(overrideValue), parsingPolicy)
|
|
309
|
+
const { lower, upper } = computeTildeBounds(String(value), parsingPolicy)
|
|
310
|
+
return compareSemverTuples(overrideParsed, lower) >= 0 && compareSemverTuples(overrideParsed, upper) < 0
|
|
311
|
+
}
|
|
312
|
+
case 'semver_caret': {
|
|
313
|
+
const overrideParsed = parseFeatureFlagSemver(String(overrideValue), parsingPolicy)
|
|
314
|
+
const { lower, upper } = computeCaretBounds(String(value), parsingPolicy)
|
|
315
|
+
return compareSemverTuples(overrideParsed, lower) >= 0 && compareSemverTuples(overrideParsed, upper) < 0
|
|
316
|
+
}
|
|
317
|
+
case 'semver_wildcard': {
|
|
318
|
+
const overrideParsed = parseFeatureFlagSemver(String(overrideValue), parsingPolicy)
|
|
319
|
+
const { lower, upper } = computeWildcardBounds(String(value), parsingPolicy)
|
|
320
|
+
return compareSemverTuples(overrideParsed, lower) >= 0 && compareSemverTuples(overrideParsed, upper) < 0
|
|
321
|
+
}
|
|
322
|
+
default:
|
|
323
|
+
throw new InconclusiveMatchError(`Unknown operator: ${operator}`)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export async function hashSHA1(text: string): Promise<string> {
|
|
328
|
+
const subtle = globalThis.crypto?.subtle
|
|
329
|
+
if (!subtle) throw new Error('SubtleCrypto API not available')
|
|
330
|
+
|
|
331
|
+
const hashBuffer = await subtle.digest('SHA-1', new TextEncoder().encode(text))
|
|
332
|
+
return Array.from(new Uint8Array(hashBuffer))
|
|
333
|
+
.map((byte) => byte.toString(16).padStart(2, '0'))
|
|
334
|
+
.join('')
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export async function getFeatureFlagHash(key: string, bucketingValue: string, salt: string = ''): Promise<number> {
|
|
338
|
+
const hashString = await hashSHA1(`${key}.${bucketingValue}${salt}`)
|
|
339
|
+
return parseInt(hashString.slice(0, 15), 16) / LONG_SCALE
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function getFeatureFlagVariantLookupTable(
|
|
343
|
+
variants: readonly FeatureFlagVariant[]
|
|
344
|
+
): FeatureFlagVariantLookupEntry[] {
|
|
345
|
+
const table: FeatureFlagVariantLookupEntry[] = []
|
|
346
|
+
let valueMin = 0
|
|
347
|
+
for (const variant of variants) {
|
|
348
|
+
const valueMax = valueMin + variant.rollout_percentage / 100.0
|
|
349
|
+
table.push({ valueMin, valueMax, key: variant.key })
|
|
350
|
+
valueMin = valueMax
|
|
351
|
+
}
|
|
352
|
+
return table
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export async function getFeatureFlagVariant(
|
|
356
|
+
key: string,
|
|
357
|
+
bucketingValue: string,
|
|
358
|
+
variants: readonly FeatureFlagVariant[]
|
|
359
|
+
): Promise<string | undefined> {
|
|
360
|
+
const hashValue = await getFeatureFlagHash(key, bucketingValue, 'variant')
|
|
361
|
+
return getFeatureFlagVariantLookupTable(variants).find(
|
|
362
|
+
(variant) => hashValue >= variant.valueMin && hashValue < variant.valueMax
|
|
363
|
+
)?.key
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function resolveFeatureFlagPayload(
|
|
367
|
+
payloads: Readonly<Record<string, JsonType | undefined>> | null | undefined,
|
|
368
|
+
flagValue: FeatureFlagValue | null | undefined
|
|
369
|
+
): JsonType | null {
|
|
370
|
+
if (flagValue === false || flagValue === null || flagValue === undefined || !payloads) return null
|
|
371
|
+
|
|
372
|
+
const payloadKey = typeof flagValue === 'boolean' ? flagValue.toString() : flagValue
|
|
373
|
+
const payload = payloads[payloadKey] || null
|
|
374
|
+
return payload == null ? null : (parsePayload(payload) as JsonType)
|
|
375
|
+
}
|
|
@@ -7,6 +7,25 @@ export {
|
|
|
7
7
|
MINIMAL_FLAG_CALLED_EVENT_CAMPAIGN_PROPERTIES,
|
|
8
8
|
minimizeFlagCalledEventProperties,
|
|
9
9
|
} from './featureFlagUtils'
|
|
10
|
+
export {
|
|
11
|
+
getFeatureFlagHash,
|
|
12
|
+
getFeatureFlagVariant,
|
|
13
|
+
getFeatureFlagVariantLookupTable,
|
|
14
|
+
hashSHA1,
|
|
15
|
+
InconclusiveMatchError,
|
|
16
|
+
matchFeatureFlagProperty,
|
|
17
|
+
parseFeatureFlagSemver,
|
|
18
|
+
relativeDateParseForFeatureFlagMatching,
|
|
19
|
+
resolveFeatureFlagPayload,
|
|
20
|
+
} from './featureFlagLocalEvaluation'
|
|
21
|
+
export type {
|
|
22
|
+
FeatureFlagProperty,
|
|
23
|
+
FeatureFlagPropertyValue,
|
|
24
|
+
FeatureFlagSemverParsingPolicy,
|
|
25
|
+
FeatureFlagVariant,
|
|
26
|
+
FeatureFlagVariantLookupEntry,
|
|
27
|
+
MatchFeatureFlagPropertyOptions,
|
|
28
|
+
} from './featureFlagLocalEvaluation'
|
|
10
29
|
export {
|
|
11
30
|
gzipCompress,
|
|
12
31
|
isGzipData,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PostHogPersistedProperty } from '../types'
|
|
2
|
+
import { createTestClient, PostHogCoreTestClient } from '../testing'
|
|
2
3
|
import type { Logger } from '../types'
|
|
3
4
|
import { PostHogLogs } from './index'
|
|
4
5
|
import type { BufferedLogEntry, ResolvedPostHogLogsConfig } from './types'
|
|
@@ -80,6 +81,82 @@ const getContextFor = (instance: any) => (): { distinctId?: string; sessionId?:
|
|
|
80
81
|
sessionId: instance.getSessionId() || undefined,
|
|
81
82
|
})
|
|
82
83
|
|
|
84
|
+
// Drives a real core host rather than a stubbed `_sendLogsBatch`, so the sender's
|
|
85
|
+
// error classification and the queue bookkeeping are exercised together.
|
|
86
|
+
describe('PostHogLogs over the core sender', () => {
|
|
87
|
+
const createLogsOverCore = (status: number): { logs: PostHogLogs; client: PostHogCoreTestClient } => {
|
|
88
|
+
const [client, mocks] = createTestClient('TEST_API_KEY', {
|
|
89
|
+
fetchRetryCount: 0,
|
|
90
|
+
preloadFeatureFlags: false,
|
|
91
|
+
})
|
|
92
|
+
mocks.fetch.mockResolvedValue({
|
|
93
|
+
status,
|
|
94
|
+
text: () => Promise.resolve('err'),
|
|
95
|
+
json: () => Promise.resolve({ status: 'err' }),
|
|
96
|
+
})
|
|
97
|
+
const logs = new PostHogLogs(
|
|
98
|
+
client,
|
|
99
|
+
resolveForTest(),
|
|
100
|
+
createMockLogger(),
|
|
101
|
+
() => ({ distinctId: 'user-123' }),
|
|
102
|
+
immediateOnReady
|
|
103
|
+
)
|
|
104
|
+
return { logs, client }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const queueOf = (client: PostHogCoreTestClient): BufferedLogEntry[] =>
|
|
108
|
+
client.getPersistedProperty<BufferedLogEntry[]>(PostHogPersistedProperty.LogsQueue) ?? []
|
|
109
|
+
|
|
110
|
+
it.each([408, 429, 500, 503])('keeps records queued when the endpoint answers %i', async (status) => {
|
|
111
|
+
const { logs, client } = createLogsOverCore(status)
|
|
112
|
+
logs.captureLog({ body: 'keep me' })
|
|
113
|
+
|
|
114
|
+
await expect(logs.flush()).rejects.toHaveProperty('name', 'PostHogFetchHttpError')
|
|
115
|
+
|
|
116
|
+
expect(queueOf(client)).toHaveLength(1)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('retains and resends records after transport retries are exhausted', async () => {
|
|
120
|
+
jest.useRealTimers()
|
|
121
|
+
const [client, mocks] = createTestClient('TEST_API_KEY', {
|
|
122
|
+
fetchRetryCount: 2,
|
|
123
|
+
fetchRetryDelay: 1,
|
|
124
|
+
preloadFeatureFlags: false,
|
|
125
|
+
})
|
|
126
|
+
const unavailableResponse = { status: 503, text: async () => 'unavailable', json: async () => ({}) }
|
|
127
|
+
mocks.fetch
|
|
128
|
+
.mockResolvedValueOnce(unavailableResponse)
|
|
129
|
+
.mockResolvedValueOnce(unavailableResponse)
|
|
130
|
+
.mockResolvedValueOnce(unavailableResponse)
|
|
131
|
+
.mockResolvedValueOnce({ status: 200, text: async () => 'ok', json: async () => ({}) })
|
|
132
|
+
const logs = new PostHogLogs(
|
|
133
|
+
client,
|
|
134
|
+
resolveForTest(),
|
|
135
|
+
createMockLogger(),
|
|
136
|
+
() => ({ distinctId: 'user-123' }),
|
|
137
|
+
immediateOnReady
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
logs.captureLog({ body: 'retry me' })
|
|
141
|
+
await expect(logs.flush()).rejects.toHaveProperty('name', 'PostHogFetchHttpError')
|
|
142
|
+
expect(mocks.fetch).toHaveBeenCalledTimes(3)
|
|
143
|
+
expect(queueOf(client)).toHaveLength(1)
|
|
144
|
+
|
|
145
|
+
await expect(logs.flush()).resolves.toBeUndefined()
|
|
146
|
+
expect(mocks.fetch).toHaveBeenCalledTimes(4)
|
|
147
|
+
expect(queueOf(client)).toHaveLength(0)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('drops the batch when the endpoint answers 401', async () => {
|
|
151
|
+
const { logs, client } = createLogsOverCore(401)
|
|
152
|
+
logs.captureLog({ body: 'unauthorized' })
|
|
153
|
+
|
|
154
|
+
await expect(logs.flush()).rejects.toHaveProperty('name', 'PostHogFetchHttpError')
|
|
155
|
+
|
|
156
|
+
expect(queueOf(client)).toHaveLength(0)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
83
160
|
describe('PostHogLogs', () => {
|
|
84
161
|
let mockInstance: any
|
|
85
162
|
let logger: Logger
|
|
@@ -262,7 +262,7 @@ export class PostHogLogs {
|
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
if (outcome.kind === 'retry-later') {
|
|
265
|
-
//
|
|
265
|
+
// Transient failure: keep records in the queue for the next flush cycle
|
|
266
266
|
// and surface the error so the caller can log/react.
|
|
267
267
|
throw outcome.error
|
|
268
268
|
}
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
FeatureFlagDetail,
|
|
18
18
|
SurveyResponse,
|
|
19
19
|
PostHogFetchResponse,
|
|
20
|
+
PostHogFetchBodyBytes,
|
|
20
21
|
PostHogFetchOptions,
|
|
21
22
|
PostHogPersistedProperty,
|
|
22
23
|
PostHogQueueItem,
|
|
@@ -221,13 +222,12 @@ function isPostHogEventProperties(value: JsonType | undefined): value is PostHog
|
|
|
221
222
|
}
|
|
222
223
|
|
|
223
224
|
/**
|
|
224
|
-
* Outcome of a logs batch send. Keeps HTTP error classification inside core
|
|
225
|
-
* (single source of truth — same policy events already use in `_flush()`) so
|
|
225
|
+
* Outcome of a logs batch send. Keeps HTTP error classification inside core so
|
|
226
226
|
* PostHogLogs doesn't need to know about specific error types.
|
|
227
227
|
*
|
|
228
228
|
* - ok → records are accepted; drop them from the queue
|
|
229
229
|
* - too-large → 413; caller should halve batch size and retry same records
|
|
230
|
-
* - retry-later → network error; caller keeps records and retries next cycle
|
|
230
|
+
* - retry-later → retryable network or HTTP error; caller keeps records and retries next cycle
|
|
231
231
|
* - fatal → anything else (auth, malformed, etc.); caller drops the
|
|
232
232
|
* batch and surfaces the error
|
|
233
233
|
*/
|
|
@@ -237,6 +237,17 @@ export type SendLogsBatchOutcome =
|
|
|
237
237
|
| { kind: 'retry-later'; error: unknown }
|
|
238
238
|
| { kind: 'fatal'; error: unknown }
|
|
239
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Each signal keeps its own exported outcome type because each belongs to a
|
|
242
|
+
* separate host contract. The wrappers return this value directly, so one
|
|
243
|
+
* drifting out of shape fails to compile.
|
|
244
|
+
*/
|
|
245
|
+
type SendOtlpBatchOutcome =
|
|
246
|
+
| { kind: 'ok' }
|
|
247
|
+
| { kind: 'too-large' }
|
|
248
|
+
| { kind: 'retry-later'; error: unknown }
|
|
249
|
+
| { kind: 'fatal'; error: unknown }
|
|
250
|
+
|
|
240
251
|
export enum QuotaLimitedFeature {
|
|
241
252
|
FeatureFlags = 'feature_flags',
|
|
242
253
|
Recordings = 'recordings',
|
|
@@ -1447,7 +1458,7 @@ export abstract class PostHogCoreStateless {
|
|
|
1447
1458
|
* Compresses an outgoing payload. Runtime-specific clients can override this
|
|
1448
1459
|
* to avoid using the Web Streams compression implementation.
|
|
1449
1460
|
*/
|
|
1450
|
-
protected compressPayload(payload: string): Promise<Blob | null> {
|
|
1461
|
+
protected compressPayload(payload: string): Promise<Blob | PostHogFetchBodyBytes | null> {
|
|
1451
1462
|
return gzipCompress(payload, this.isDebug)
|
|
1452
1463
|
}
|
|
1453
1464
|
|
|
@@ -1632,22 +1643,28 @@ export abstract class PostHogCoreStateless {
|
|
|
1632
1643
|
}
|
|
1633
1644
|
|
|
1634
1645
|
/**
|
|
1635
|
-
*
|
|
1636
|
-
* outcome instead of throwing so
|
|
1637
|
-
* core's error class hierarchy.
|
|
1638
|
-
* source of truth, same policy the events `_flush()` uses for its own
|
|
1639
|
-
* 413 / network / fatal handling).
|
|
1646
|
+
* Shared implementation behind the OTLP senders, which differ only in path.
|
|
1647
|
+
* Returns a tagged outcome instead of throwing so the queue owners don't
|
|
1648
|
+
* have to know the core's error class hierarchy.
|
|
1640
1649
|
*
|
|
1641
|
-
*
|
|
1642
|
-
*
|
|
1650
|
+
* Exhausted 408/429/5xx stay `retry-later`, unlike the events `_flush()`
|
|
1651
|
+
* which drops anything that isn't a network error: every OTLP queue is
|
|
1652
|
+
* bounded and retried with backoff, so holding a batch through an outage can
|
|
1653
|
+
* neither grow without limit nor spin.
|
|
1643
1654
|
*/
|
|
1644
|
-
async
|
|
1655
|
+
private async _sendOtlpBatch({
|
|
1656
|
+
path,
|
|
1657
|
+
payload,
|
|
1658
|
+
}: {
|
|
1659
|
+
path: 'logs' | 'metrics'
|
|
1660
|
+
payload: OtlpLogsPayload | OtlpMetricsPayload
|
|
1661
|
+
}): Promise<SendOtlpBatchOutcome> {
|
|
1645
1662
|
if (this.disabled) {
|
|
1646
1663
|
return { kind: 'fatal', error: new Error('The client is disabled') }
|
|
1647
1664
|
}
|
|
1648
1665
|
|
|
1649
1666
|
const serialized = JSON.stringify(payload)
|
|
1650
|
-
const url = `${this.host}/i/v1
|
|
1667
|
+
const url = `${this.host}/i/v1/${path}?token=${encodeURIComponent(this.apiKey)}`
|
|
1651
1668
|
|
|
1652
1669
|
const gzippedPayload = !this.disableCompression ? await this.compressPayload(serialized) : null
|
|
1653
1670
|
const fetchOptions: PostHogFetchOptions = {
|
|
@@ -1679,65 +1696,19 @@ export abstract class PostHogCoreStateless {
|
|
|
1679
1696
|
if (isPostHogFetchContentTooLargeError(err)) {
|
|
1680
1697
|
return { kind: 'too-large' }
|
|
1681
1698
|
}
|
|
1682
|
-
if (err
|
|
1699
|
+
if (isPostHogFetchRetryableError(err)) {
|
|
1683
1700
|
return { kind: 'retry-later', error: err }
|
|
1684
1701
|
}
|
|
1685
1702
|
return { kind: 'fatal', error: err }
|
|
1686
1703
|
}
|
|
1687
1704
|
}
|
|
1688
1705
|
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
* the `MetricsHost._sendMetricsBatch` implementation, so `PostHogMetrics`
|
|
1693
|
-
* can use any core-based SDK as its host.
|
|
1694
|
-
*/
|
|
1695
|
-
async _sendMetricsBatch(payload: OtlpMetricsPayload): Promise<SendMetricsBatchOutcome> {
|
|
1696
|
-
if (this.disabled) {
|
|
1697
|
-
return { kind: 'fatal', error: new Error('The client is disabled') }
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
const serialized = JSON.stringify(payload)
|
|
1701
|
-
const url = `${this.host}/i/v1/metrics?token=${encodeURIComponent(this.apiKey)}`
|
|
1702
|
-
|
|
1703
|
-
const gzippedPayload = !this.disableCompression ? await this.compressPayload(serialized) : null
|
|
1704
|
-
const fetchOptions: PostHogFetchOptions = {
|
|
1705
|
-
method: 'POST',
|
|
1706
|
-
headers: {
|
|
1707
|
-
...this.getCustomHeaders(),
|
|
1708
|
-
'Content-Type': 'application/json',
|
|
1709
|
-
...(gzippedPayload !== null && { 'Content-Encoding': 'gzip' }),
|
|
1710
|
-
},
|
|
1711
|
-
body: gzippedPayload || serialized,
|
|
1712
|
-
}
|
|
1706
|
+
async _sendLogsBatch(payload: OtlpLogsPayload): Promise<SendLogsBatchOutcome> {
|
|
1707
|
+
return this._sendOtlpBatch({ path: 'logs', payload })
|
|
1708
|
+
}
|
|
1713
1709
|
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
url,
|
|
1717
|
-
fetchOptions,
|
|
1718
|
-
{ type: 'successful-write' },
|
|
1719
|
-
{
|
|
1720
|
-
retryCheck: (err) => {
|
|
1721
|
-
if (isPostHogFetchContentTooLargeError(err)) {
|
|
1722
|
-
return false
|
|
1723
|
-
}
|
|
1724
|
-
return isPostHogFetchRetryableError(err)
|
|
1725
|
-
},
|
|
1726
|
-
}
|
|
1727
|
-
)
|
|
1728
|
-
return { kind: 'ok' }
|
|
1729
|
-
} catch (err) {
|
|
1730
|
-
if (isPostHogFetchContentTooLargeError(err)) {
|
|
1731
|
-
return { kind: 'too-large' }
|
|
1732
|
-
}
|
|
1733
|
-
// Exhausted retries on a retryable failure (network error, 408/429/5xx)
|
|
1734
|
-
// still classify as retry-later so the window rides the next flush; only
|
|
1735
|
-
// non-retryable HTTP errors (and 413 above) drop the batch.
|
|
1736
|
-
if (isPostHogFetchRetryableError(err)) {
|
|
1737
|
-
return { kind: 'retry-later', error: err }
|
|
1738
|
-
}
|
|
1739
|
-
return { kind: 'fatal', error: err }
|
|
1740
|
-
}
|
|
1710
|
+
async _sendMetricsBatch(payload: OtlpMetricsPayload): Promise<SendMetricsBatchOutcome> {
|
|
1711
|
+
return this._sendOtlpBatch({ path: 'metrics', payload })
|
|
1741
1712
|
}
|
|
1742
1713
|
|
|
1743
1714
|
private fetchWithRetry<T>(
|
|
@@ -1766,12 +1737,16 @@ export abstract class PostHogCoreStateless {
|
|
|
1766
1737
|
try {
|
|
1767
1738
|
if (body instanceof Blob) {
|
|
1768
1739
|
reqByteLength = body.size
|
|
1740
|
+
} else if (body instanceof Uint8Array) {
|
|
1741
|
+
reqByteLength = body.byteLength
|
|
1769
1742
|
} else {
|
|
1770
1743
|
reqByteLength = Buffer.byteLength(body, STRING_FORMAT)
|
|
1771
1744
|
}
|
|
1772
1745
|
} catch {
|
|
1773
1746
|
if (body instanceof Blob) {
|
|
1774
1747
|
reqByteLength = body.size
|
|
1748
|
+
} else if (body instanceof Uint8Array) {
|
|
1749
|
+
reqByteLength = body.byteLength
|
|
1775
1750
|
} else {
|
|
1776
1751
|
const encoded = new TextEncoder().encode(body)
|
|
1777
1752
|
reqByteLength = encoded.length
|
|
@@ -18,3 +18,4 @@ export {
|
|
|
18
18
|
} from './translations'
|
|
19
19
|
export { canSurveyActivateRepeatedly, doesSurveyActivateByEvent, isSurveyIterationBased } from './activation'
|
|
20
20
|
export { getSurveyIterationKey, isSurveyKeyForSurvey, type SurveyWithIteration } from './keys'
|
|
21
|
+
export { isMatchingRegex, isValidRegex, matchPropertyFilters, propertyComparisons } from './property-matching'
|