@posthog/core 1.49.2 → 1.50.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/featureFlagLocalEvaluation.d.ts +1 -1
- package/dist/featureFlagLocalEvaluation.d.ts.map +1 -1
- package/dist/featureFlagLocalEvaluation.js +149 -10
- package/dist/featureFlagLocalEvaluation.mjs +149 -10
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -31
- package/dist/index.mjs +2 -1
- package/dist/logs/logs-utils.d.ts +1 -3
- package/dist/logs/logs-utils.d.ts.map +1 -1
- package/dist/logs/logs-utils.js +5 -158
- package/dist/logs/logs-utils.mjs +4 -151
- package/dist/metrics/index.js +2 -2
- package/dist/metrics/index.mjs +1 -1
- package/dist/metrics/metrics-utils.js +2 -2
- package/dist/metrics/metrics-utils.mjs +1 -1
- package/dist/utils/otlp-any-value.d.ts +5 -0
- package/dist/utils/otlp-any-value.d.ts.map +1 -0
- package/dist/utils/otlp-any-value.js +203 -0
- package/dist/utils/otlp-any-value.mjs +166 -0
- package/dist/utils/user-agent-utils.d.ts +4 -4
- package/dist/utils/user-agent-utils.d.ts.map +1 -1
- package/dist/utils/user-agent-utils.js +16 -0
- package/dist/utils/user-agent-utils.mjs +16 -0
- package/package.json +1 -1
- package/src/featureFlagLocalEvaluation.ts +199 -12
- package/src/index.ts +1 -2
- package/src/logs/logs-utils.spec.ts +2 -350
- package/src/logs/logs-utils.ts +3 -207
- package/src/metrics/index.ts +1 -1
- package/src/metrics/metrics-utils.ts +1 -1
- package/src/utils/otlp-any-value.spec.ts +361 -0
- package/src/utils/otlp-any-value.ts +225 -0
- package/src/utils/user-agent-utils.ts +22 -6
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { isArray, isBoolean, isNull, isNullish, isUndefined } from "./type-utils.mjs";
|
|
2
|
+
import { CIRCULAR_VALUE, FUNCTION_VALUE, MAX_JSON_SAFE_VALUE_DEPTH, MAX_JSON_SAFE_VALUE_ITEMS, MAX_JSON_SAFE_VALUE_NODES, TRUNCATED_VALUE, UNSERIALIZABLE_VALUE, sanitizeString } from "./json-utils.mjs";
|
|
3
|
+
const INT64_RANGE_LIMIT = 9223372036854775808;
|
|
4
|
+
const INT64_RANGE_LIMIT_DECIMAL = '9223372036854775808';
|
|
5
|
+
const propertyIsEnumerable = Object.prototype.propertyIsEnumerable;
|
|
6
|
+
function newState() {
|
|
7
|
+
return {
|
|
8
|
+
ancestors: new WeakSet(),
|
|
9
|
+
remainingNodes: MAX_JSON_SAFE_VALUE_NODES
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function toOtlpAnyValue(value, logger) {
|
|
13
|
+
try {
|
|
14
|
+
return encodeAnyValue(value, logger, newState(), 0);
|
|
15
|
+
} catch {
|
|
16
|
+
return {
|
|
17
|
+
stringValue: UNSERIALIZABLE_VALUE
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function toOtlpKeyValueList(attrs, logger) {
|
|
22
|
+
try {
|
|
23
|
+
return encodeKeyValueList(attrs, logger, newState(), 0);
|
|
24
|
+
} catch {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function encodeBigInt(value, logger) {
|
|
29
|
+
const decimal = value.toString();
|
|
30
|
+
const limit = BigInt(INT64_RANGE_LIMIT_DECIMAL);
|
|
31
|
+
if (value >= limit || value < -limit) {
|
|
32
|
+
logger?.debug(`Attribute ${decimal} is outside the int64 range; encoding it as a string`);
|
|
33
|
+
return {
|
|
34
|
+
stringValue: decimal
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
intValue: decimal
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function encodeAnyValue(value, logger, state, depth) {
|
|
42
|
+
if (state.remainingNodes <= 0) return {
|
|
43
|
+
stringValue: TRUNCATED_VALUE
|
|
44
|
+
};
|
|
45
|
+
state.remainingNodes--;
|
|
46
|
+
if (isBoolean(value)) return {
|
|
47
|
+
boolValue: value
|
|
48
|
+
};
|
|
49
|
+
if ('bigint' == typeof value) return encodeBigInt(value, logger);
|
|
50
|
+
if ('number' == typeof value) {
|
|
51
|
+
if (!Number.isFinite(value)) return {
|
|
52
|
+
stringValue: String(value)
|
|
53
|
+
};
|
|
54
|
+
if (Number.isInteger(value)) {
|
|
55
|
+
if (Number.isSafeInteger(value)) return {
|
|
56
|
+
intValue: String(value)
|
|
57
|
+
};
|
|
58
|
+
if ('undefined' == typeof BigInt) return {
|
|
59
|
+
stringValue: String(value)
|
|
60
|
+
};
|
|
61
|
+
const decimal = BigInt(value).toString();
|
|
62
|
+
if (value >= INT64_RANGE_LIMIT || value < -INT64_RANGE_LIMIT) {
|
|
63
|
+
logger?.debug(`Attribute ${decimal} is outside the int64 range; encoding it as a string`);
|
|
64
|
+
return {
|
|
65
|
+
stringValue: decimal
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
intValue: decimal
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
doubleValue: value
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if ('string' == typeof value) return {
|
|
77
|
+
stringValue: sanitizeString(value)
|
|
78
|
+
};
|
|
79
|
+
if ('function' == typeof value) return {
|
|
80
|
+
stringValue: FUNCTION_VALUE
|
|
81
|
+
};
|
|
82
|
+
if ('symbol' == typeof value) return {
|
|
83
|
+
stringValue: String(value)
|
|
84
|
+
};
|
|
85
|
+
if ('object' == typeof value && null !== value) {
|
|
86
|
+
if (state.ancestors.has(value)) return {
|
|
87
|
+
stringValue: CIRCULAR_VALUE
|
|
88
|
+
};
|
|
89
|
+
if (depth >= MAX_JSON_SAFE_VALUE_DEPTH) return {
|
|
90
|
+
stringValue: TRUNCATED_VALUE
|
|
91
|
+
};
|
|
92
|
+
if (value instanceof Date) {
|
|
93
|
+
const time = value.getTime();
|
|
94
|
+
const iso = Number.isFinite(time) ? value.toISOString() : String(value);
|
|
95
|
+
return {
|
|
96
|
+
stringValue: 'string' == typeof iso ? sanitizeString(iso) : String(iso)
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
state.ancestors.add(value);
|
|
100
|
+
try {
|
|
101
|
+
try {
|
|
102
|
+
const toJSON = value.toJSON;
|
|
103
|
+
if ('function' == typeof toJSON) return encodeAnyValue(toJSON.call(value), logger, state, depth + 1);
|
|
104
|
+
} catch {}
|
|
105
|
+
if (isArray(value)) return {
|
|
106
|
+
arrayValue: {
|
|
107
|
+
values: encodeArrayValues(value, logger, state, depth + 1)
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
return {
|
|
111
|
+
kvlistValue: {
|
|
112
|
+
values: encodeKeyValueList(value, logger, state, depth + 1)
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
} finally{
|
|
116
|
+
state.ancestors.delete(value);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
stringValue: sanitizeString(String(value))
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function encodeArrayValues(values, logger, state, depth) {
|
|
124
|
+
const result = [];
|
|
125
|
+
const itemCount = Math.min(values.length, MAX_JSON_SAFE_VALUE_ITEMS);
|
|
126
|
+
let index = 0;
|
|
127
|
+
for(; index < itemCount && state.remainingNodes > 0; index++)try {
|
|
128
|
+
const element = index in values ? values[index] : void 0;
|
|
129
|
+
if (isNullish(element)) continue;
|
|
130
|
+
result.push(encodeAnyValue(element, logger, state, depth));
|
|
131
|
+
} catch {
|
|
132
|
+
result.push({
|
|
133
|
+
stringValue: UNSERIALIZABLE_VALUE
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (values.length > index) result.push({
|
|
137
|
+
stringValue: TRUNCATED_VALUE
|
|
138
|
+
});
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
function encodeKeyValueList(attrs, logger, state, depth) {
|
|
142
|
+
const result = [];
|
|
143
|
+
for(const key in attrs)if (propertyIsEnumerable.call(attrs, key)) {
|
|
144
|
+
if (result.length >= MAX_JSON_SAFE_VALUE_ITEMS || state.remainingNodes <= 0) {
|
|
145
|
+
logger?.debug('Attributes truncated: the value exceeds the OTLP encoder budget');
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const value = attrs[key];
|
|
150
|
+
if (isNull(value) || isUndefined(value)) continue;
|
|
151
|
+
result.push({
|
|
152
|
+
key: sanitizeString(key),
|
|
153
|
+
value: encodeAnyValue(value, logger, state, depth)
|
|
154
|
+
});
|
|
155
|
+
} catch {
|
|
156
|
+
result.push({
|
|
157
|
+
key: sanitizeString(key),
|
|
158
|
+
value: {
|
|
159
|
+
stringValue: UNSERIALIZABLE_VALUE
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
export { toOtlpAnyValue, toOtlpKeyValueList };
|
|
@@ -8,10 +8,10 @@ export interface BrowserDetectionHints {
|
|
|
8
8
|
brave?: boolean;
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
|
-
* Opt-in tweaks to UA-string detection.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* Opt-in tweaks to UA-string detection. Turning one on reattributes browsers
|
|
12
|
+
* that were previously reported as something else, so the host SDK gates
|
|
13
|
+
* shifts big enough to move users' metrics behind its `2026-05-30` config
|
|
14
|
+
* defaults. Smaller reattributions ship unconditionally in `detectBrowser`.
|
|
15
15
|
*/
|
|
16
16
|
export interface BrowserDetectionOptions {
|
|
17
17
|
detectGoogleSearchApp?: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user-agent-utils.d.ts","sourceRoot":"","sources":["../../src/utils/user-agent-utils.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"user-agent-utils.d.ts","sourceRoot":"","sources":["../../src/utils/user-agent-utils.ts"],"names":[],"mappings":"AAsEA;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IAGpC,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AASD;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IAGtC,qBAAqB,CAAC,EAAE,OAAO,CAAA;CAChC;AAgCD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,aAAa,GACxB,YAAY,MAAM,EAClB,QAAQ,MAAM,GAAG,SAAS,EAC1B,QAAQ,qBAAqB,EAC7B,UAAU,uBAAuB,KAChC,MAkGF,CAAA;AAsCD;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAC/B,WAAW,MAAM,EACjB,QAAQ,MAAM,GAAG,SAAS,EAC1B,QAAQ,qBAAqB,EAC7B,UAAU,uBAAuB,KAChC,MAAM,GAAG,IAmBX,CAAA;AA0FD,eAAO,MAAM,QAAQ,GAAa,YAAY,MAAM,KAAG,CAAC,MAAM,EAAE,MAAM,CAUrE,CAAA;AAED,eAAO,MAAM,YAAY,GAAa,YAAY,MAAM,KAAG,MAuD1D,CAAA;AAED,eAAO,MAAM,gBAAgB,GAC3B,YAAY,MAAM,EAClB,UAAU;IACR,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B,KACA,MA4BF,CAAA"}
|
|
@@ -78,9 +78,13 @@ const DUCKDUCKGO = 'DuckDuckGo';
|
|
|
78
78
|
const PALE_MOON = 'Pale Moon';
|
|
79
79
|
const WATERFOX = 'Waterfox';
|
|
80
80
|
const BRAVE = 'Brave';
|
|
81
|
+
const CLAUDE = 'Claude';
|
|
82
|
+
const CODEX = 'Codex';
|
|
83
|
+
const CHATGPT = 'ChatGPT';
|
|
81
84
|
const GOOGLE_SEARCH_APP = 'Google Search App';
|
|
82
85
|
const BROWSER_VERSION_REGEX_SUFFIX = '(\\d+(\\.\\d+)?)';
|
|
83
86
|
const DEFAULT_BROWSER_VERSION_REGEX = new RegExp('Version/' + BROWSER_VERSION_REGEX_SUFFIX);
|
|
87
|
+
const AI_APP_VERSION_REGEX = new RegExp('(' + CLAUDE + '|' + CODEX + '|' + CHATGPT + ')\\/' + BROWSER_VERSION_REGEX_SUFFIX);
|
|
84
88
|
function browserFromHints(hints) {
|
|
85
89
|
if (hints?.brave) return BRAVE;
|
|
86
90
|
return null;
|
|
@@ -122,6 +126,9 @@ const detectBrowser = function(user_agent, vendor, hints, options) {
|
|
|
122
126
|
else if ((0, external_string_utils_js_namespaceObject.includes)(user_agent, 'YaBrowser/')) return YANDEX;
|
|
123
127
|
else if ((0, external_string_utils_js_namespaceObject.includes)(user_agent, WHALE + '/')) return WHALE;
|
|
124
128
|
else if ((0, external_string_utils_js_namespaceObject.includes)(user_agent, DUCKDUCKGO + '/') || (0, external_string_utils_js_namespaceObject.includes)(user_agent, 'Ddg/')) return DUCKDUCKGO;
|
|
129
|
+
else if ((0, external_string_utils_js_namespaceObject.includes)(user_agent, CLAUDE + '/')) return CLAUDE;
|
|
130
|
+
else if ((0, external_string_utils_js_namespaceObject.includes)(user_agent, CODEX + '/')) return CODEX;
|
|
131
|
+
else if ((0, external_string_utils_js_namespaceObject.includes)(user_agent, CHATGPT + '/')) return CHATGPT;
|
|
125
132
|
else if ((0, external_string_utils_js_namespaceObject.includes)(user_agent, 'FBIOS')) return FACEBOOK + ' ' + MOBILE;
|
|
126
133
|
else if ((0, external_string_utils_js_namespaceObject.includes)(user_agent, 'UCWEB') || (0, external_string_utils_js_namespaceObject.includes)(user_agent, 'UCBrowser')) return 'UC Browser';
|
|
127
134
|
else if ((0, external_string_utils_js_namespaceObject.includes)(user_agent, 'CriOS')) return CHROME_IOS;
|
|
@@ -198,6 +205,15 @@ const versionRegexes = {
|
|
|
198
205
|
[BRAVE]: [
|
|
199
206
|
new RegExp(BRAVE + '\\/' + BROWSER_VERSION_REGEX_SUFFIX)
|
|
200
207
|
],
|
|
208
|
+
[CLAUDE]: [
|
|
209
|
+
AI_APP_VERSION_REGEX
|
|
210
|
+
],
|
|
211
|
+
[CODEX]: [
|
|
212
|
+
AI_APP_VERSION_REGEX
|
|
213
|
+
],
|
|
214
|
+
[CHATGPT]: [
|
|
215
|
+
AI_APP_VERSION_REGEX
|
|
216
|
+
],
|
|
201
217
|
[DUCKDUCKGO]: [
|
|
202
218
|
new RegExp('(DuckDuckGo|Ddg)\\/' + BROWSER_VERSION_REGEX_SUFFIX)
|
|
203
219
|
],
|
|
@@ -46,9 +46,13 @@ const DUCKDUCKGO = 'DuckDuckGo';
|
|
|
46
46
|
const PALE_MOON = 'Pale Moon';
|
|
47
47
|
const WATERFOX = 'Waterfox';
|
|
48
48
|
const BRAVE = 'Brave';
|
|
49
|
+
const CLAUDE = 'Claude';
|
|
50
|
+
const CODEX = 'Codex';
|
|
51
|
+
const CHATGPT = 'ChatGPT';
|
|
49
52
|
const GOOGLE_SEARCH_APP = 'Google Search App';
|
|
50
53
|
const BROWSER_VERSION_REGEX_SUFFIX = '(\\d+(\\.\\d+)?)';
|
|
51
54
|
const DEFAULT_BROWSER_VERSION_REGEX = new RegExp('Version/' + BROWSER_VERSION_REGEX_SUFFIX);
|
|
55
|
+
const AI_APP_VERSION_REGEX = new RegExp('(' + CLAUDE + '|' + CODEX + '|' + CHATGPT + ')\\/' + BROWSER_VERSION_REGEX_SUFFIX);
|
|
52
56
|
function browserFromHints(hints) {
|
|
53
57
|
if (hints?.brave) return BRAVE;
|
|
54
58
|
return null;
|
|
@@ -90,6 +94,9 @@ const detectBrowser = function(user_agent, vendor, hints, options) {
|
|
|
90
94
|
else if (includes(user_agent, 'YaBrowser/')) return YANDEX;
|
|
91
95
|
else if (includes(user_agent, WHALE + '/')) return WHALE;
|
|
92
96
|
else if (includes(user_agent, DUCKDUCKGO + '/') || includes(user_agent, 'Ddg/')) return DUCKDUCKGO;
|
|
97
|
+
else if (includes(user_agent, CLAUDE + '/')) return CLAUDE;
|
|
98
|
+
else if (includes(user_agent, CODEX + '/')) return CODEX;
|
|
99
|
+
else if (includes(user_agent, CHATGPT + '/')) return CHATGPT;
|
|
93
100
|
else if (includes(user_agent, 'FBIOS')) return FACEBOOK + ' ' + MOBILE;
|
|
94
101
|
else if (includes(user_agent, 'UCWEB') || includes(user_agent, 'UCBrowser')) return 'UC Browser';
|
|
95
102
|
else if (includes(user_agent, 'CriOS')) return CHROME_IOS;
|
|
@@ -166,6 +173,15 @@ const versionRegexes = {
|
|
|
166
173
|
[BRAVE]: [
|
|
167
174
|
new RegExp(BRAVE + '\\/' + BROWSER_VERSION_REGEX_SUFFIX)
|
|
168
175
|
],
|
|
176
|
+
[CLAUDE]: [
|
|
177
|
+
AI_APP_VERSION_REGEX
|
|
178
|
+
],
|
|
179
|
+
[CODEX]: [
|
|
180
|
+
AI_APP_VERSION_REGEX
|
|
181
|
+
],
|
|
182
|
+
[CHATGPT]: [
|
|
183
|
+
AI_APP_VERSION_REGEX
|
|
184
|
+
],
|
|
169
185
|
[DUCKDUCKGO]: [
|
|
170
186
|
new RegExp('(DuckDuckGo|Ddg)\\/' + BROWSER_VERSION_REGEX_SUFFIX)
|
|
171
187
|
],
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parsePayload } from './featureFlagUtils'
|
|
2
2
|
import type { FeatureFlagValue, JsonType } from './types'
|
|
3
3
|
|
|
4
|
-
export type FeatureFlagPropertyValue =
|
|
4
|
+
export type FeatureFlagPropertyValue = JsonType
|
|
5
5
|
|
|
6
6
|
export type FeatureFlagProperty = {
|
|
7
7
|
key: string
|
|
@@ -42,6 +42,172 @@ export class InconclusiveMatchError extends Error {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
function isTruthyOrFalsyPropertyValue(value: unknown): boolean {
|
|
46
|
+
if (typeof value === 'boolean') return true
|
|
47
|
+
if (typeof value === 'string') {
|
|
48
|
+
const lowercaseValue = value.toLowerCase()
|
|
49
|
+
return lowercaseValue === 'true' || lowercaseValue === 'false'
|
|
50
|
+
}
|
|
51
|
+
if (!Array.isArray(value)) return false
|
|
52
|
+
for (let index = 0; index < value.length; index++) {
|
|
53
|
+
if (!isTruthyOrFalsyPropertyValue(index in value ? value[index] : null)) return false
|
|
54
|
+
}
|
|
55
|
+
return true
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isTruthyPropertyValue(value: unknown): boolean {
|
|
59
|
+
if (typeof value === 'boolean') return value
|
|
60
|
+
if (typeof value === 'string') return value.toLowerCase() === 'true'
|
|
61
|
+
if (!Array.isArray(value)) return false
|
|
62
|
+
for (let index = 0; index < value.length; index++) {
|
|
63
|
+
if (!isTruthyPropertyValue(index in value ? value[index] : null)) return false
|
|
64
|
+
}
|
|
65
|
+
return true
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function assertUnicodeScalarString(value: string): void {
|
|
69
|
+
for (let index = 0; index < value.length; index++) {
|
|
70
|
+
const unit = value.charCodeAt(index)
|
|
71
|
+
if (unit >= 0xd800 && unit <= 0xdbff) {
|
|
72
|
+
const next = value.charCodeAt(index + 1)
|
|
73
|
+
if (index + 1 >= value.length || next < 0xdc00 || next > 0xdfff) {
|
|
74
|
+
throw new InconclusiveMatchError('Cannot stringify an unpaired surrogate like the flags service')
|
|
75
|
+
}
|
|
76
|
+
index++
|
|
77
|
+
} else if (unit >= 0xdc00 && unit <= 0xdfff) {
|
|
78
|
+
throw new InconclusiveMatchError('Cannot stringify an unpaired surrogate like the flags service')
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function assertJsonRepresentable(value: unknown, seen: Set<object> = new Set()): void {
|
|
84
|
+
if (value === null || typeof value === 'boolean') return
|
|
85
|
+
if (typeof value === 'string') {
|
|
86
|
+
assertUnicodeScalarString(value)
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
if (typeof value === 'number') {
|
|
90
|
+
if (!Number.isFinite(value)) {
|
|
91
|
+
throw new InconclusiveMatchError(`Cannot represent non-finite number ${value} like the flags service`)
|
|
92
|
+
}
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
if (Array.isArray(value)) {
|
|
96
|
+
if (seen.has(value)) throw new InconclusiveMatchError('Cannot represent a circular array during local evaluation')
|
|
97
|
+
seen.add(value)
|
|
98
|
+
try {
|
|
99
|
+
for (let index = 0; index < value.length; index++) {
|
|
100
|
+
if (index in value) assertJsonRepresentable(value[index], seen)
|
|
101
|
+
}
|
|
102
|
+
} finally {
|
|
103
|
+
seen.delete(value)
|
|
104
|
+
}
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
if (typeof value === 'object') {
|
|
108
|
+
const prototype = Object.getPrototypeOf(value)
|
|
109
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
110
|
+
throw new InconclusiveMatchError('Cannot represent a non-JSON object like the flags service')
|
|
111
|
+
}
|
|
112
|
+
if (seen.has(value)) throw new InconclusiveMatchError('Cannot represent a circular object during local evaluation')
|
|
113
|
+
seen.add(value)
|
|
114
|
+
try {
|
|
115
|
+
for (const key of Object.keys(value)) {
|
|
116
|
+
assertUnicodeScalarString(key)
|
|
117
|
+
assertJsonRepresentable((value as Record<string, unknown>)[key], seen)
|
|
118
|
+
}
|
|
119
|
+
} finally {
|
|
120
|
+
seen.delete(value)
|
|
121
|
+
}
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
throw new InconclusiveMatchError(`Cannot represent ${typeof value} like the flags service`)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function compareJsonObjectKeys(left: string, right: string): number {
|
|
128
|
+
let leftIndex = 0
|
|
129
|
+
let rightIndex = 0
|
|
130
|
+
while (leftIndex < left.length && rightIndex < right.length) {
|
|
131
|
+
const leftUnit = left.charCodeAt(leftIndex)
|
|
132
|
+
const rightUnit = right.charCodeAt(rightIndex)
|
|
133
|
+
const leftIsHighSurrogate = leftUnit >= 0xd800 && leftUnit <= 0xdbff
|
|
134
|
+
const rightIsHighSurrogate = rightUnit >= 0xd800 && rightUnit <= 0xdbff
|
|
135
|
+
const leftNext = leftIsHighSurrogate ? left.charCodeAt(leftIndex + 1) : 0
|
|
136
|
+
const rightNext = rightIsHighSurrogate ? right.charCodeAt(rightIndex + 1) : 0
|
|
137
|
+
|
|
138
|
+
const leftCodePoint = leftIsHighSurrogate ? (leftUnit - 0xd800) * 0x400 + leftNext - 0xdc00 + 0x10000 : leftUnit
|
|
139
|
+
const rightCodePoint = rightIsHighSurrogate
|
|
140
|
+
? (rightUnit - 0xd800) * 0x400 + rightNext - 0xdc00 + 0x10000
|
|
141
|
+
: rightUnit
|
|
142
|
+
if (leftCodePoint !== rightCodePoint) return leftCodePoint - rightCodePoint
|
|
143
|
+
leftIndex += leftIsHighSurrogate ? 2 : 1
|
|
144
|
+
rightIndex += rightIsHighSurrogate ? 2 : 1
|
|
145
|
+
}
|
|
146
|
+
return left.length - right.length
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// JavaScript collapses JSON integers and integral floats into Number, including inside composites.
|
|
150
|
+
// These values fall back rather than choosing a spelling that can disagree with the flags service.
|
|
151
|
+
function serializeJsonValue(value: unknown, seen: Set<object> = new Set()): string {
|
|
152
|
+
if (value === null) return 'null'
|
|
153
|
+
if (typeof value === 'string') {
|
|
154
|
+
assertUnicodeScalarString(value)
|
|
155
|
+
return JSON.stringify(value)
|
|
156
|
+
}
|
|
157
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false'
|
|
158
|
+
if (typeof value === 'number') {
|
|
159
|
+
if (!Number.isFinite(value)) {
|
|
160
|
+
throw new InconclusiveMatchError(`Cannot stringify non-finite number ${value} like the flags service`)
|
|
161
|
+
}
|
|
162
|
+
if (Number.isInteger(value)) {
|
|
163
|
+
throw new InconclusiveMatchError(
|
|
164
|
+
`Cannot distinguish integer ${value} from an integral JSON float during local evaluation`
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
return String(value)
|
|
168
|
+
}
|
|
169
|
+
if (Array.isArray(value)) {
|
|
170
|
+
if (seen.has(value)) throw new InconclusiveMatchError('Cannot stringify a circular array during local evaluation')
|
|
171
|
+
seen.add(value)
|
|
172
|
+
try {
|
|
173
|
+
const items: string[] = []
|
|
174
|
+
for (let index = 0; index < value.length; index++) {
|
|
175
|
+
items.push(index in value ? serializeJsonValue(value[index], seen) : 'null')
|
|
176
|
+
}
|
|
177
|
+
return `[${items.join(',')}]`
|
|
178
|
+
} finally {
|
|
179
|
+
seen.delete(value)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (typeof value === 'object') {
|
|
183
|
+
const prototype = Object.getPrototypeOf(value)
|
|
184
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
185
|
+
throw new InconclusiveMatchError('Cannot stringify a non-JSON object like the flags service')
|
|
186
|
+
}
|
|
187
|
+
if (seen.has(value)) throw new InconclusiveMatchError('Cannot stringify a circular object during local evaluation')
|
|
188
|
+
seen.add(value)
|
|
189
|
+
try {
|
|
190
|
+
const keys = Object.keys(value)
|
|
191
|
+
keys.forEach(assertUnicodeScalarString)
|
|
192
|
+
return `{${keys
|
|
193
|
+
.sort(compareJsonObjectKeys)
|
|
194
|
+
.map((key) => `${JSON.stringify(key)}:${serializeJsonValue((value as Record<string, unknown>)[key], seen)}`)
|
|
195
|
+
.join(',')}}`
|
|
196
|
+
} finally {
|
|
197
|
+
seen.delete(value)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
throw new InconclusiveMatchError(`Cannot stringify ${typeof value} like the flags service`)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function exactMatchString(value: unknown): string {
|
|
204
|
+
if (typeof value === 'string') {
|
|
205
|
+
assertUnicodeScalarString(value)
|
|
206
|
+
return value
|
|
207
|
+
}
|
|
208
|
+
return serializeJsonValue(value)
|
|
209
|
+
}
|
|
210
|
+
|
|
45
211
|
function isValidRegex(regex: string): boolean {
|
|
46
212
|
try {
|
|
47
213
|
new RegExp(regex)
|
|
@@ -51,6 +217,11 @@ function isValidRegex(regex: string): boolean {
|
|
|
51
217
|
}
|
|
52
218
|
}
|
|
53
219
|
|
|
220
|
+
// The flags service deliberately folds only ASCII for substring, prefix, and suffix operators.
|
|
221
|
+
function asciiLowercase(value: unknown): string {
|
|
222
|
+
return String(value).replace(/[A-Z]/g, (character) => character.toLowerCase())
|
|
223
|
+
}
|
|
224
|
+
|
|
54
225
|
type SemverTuple = [number, number, number]
|
|
55
226
|
|
|
56
227
|
function parseSemverNumericIdentifier(
|
|
@@ -190,19 +361,35 @@ export function matchFeatureFlagProperty(
|
|
|
190
361
|
throw new InconclusiveMatchError(`Property ${key} not found in propertyValues`)
|
|
191
362
|
} else if (operator === 'is_not_set') {
|
|
192
363
|
return false
|
|
364
|
+
} else if (operator === 'is_set') {
|
|
365
|
+
return true
|
|
193
366
|
}
|
|
194
367
|
|
|
195
368
|
const overrideValue = propertyValues[key]
|
|
196
|
-
if (overrideValue
|
|
197
|
-
options.warnFunction?.(`Property ${key} cannot have a value of
|
|
369
|
+
if (overrideValue === undefined) {
|
|
370
|
+
options.warnFunction?.(`Property ${key} cannot have a value of undefined with the ${operator} operator`)
|
|
371
|
+
return operator === 'is_not'
|
|
372
|
+
}
|
|
373
|
+
if (
|
|
374
|
+
overrideValue === null &&
|
|
375
|
+
!NULL_VALUES_ALLOWED_OPERATORS.includes(operator) &&
|
|
376
|
+
operator !== 'exact' &&
|
|
377
|
+
operator !== 'is_not'
|
|
378
|
+
) {
|
|
379
|
+
options.warnFunction?.(`Property ${key} cannot have a value of null with the ${operator} operator`)
|
|
198
380
|
return false
|
|
199
381
|
}
|
|
200
382
|
|
|
201
|
-
const computeExactMatch = (target:
|
|
383
|
+
const computeExactMatch = (target: unknown, actual: unknown): boolean => {
|
|
384
|
+
if (isTruthyOrFalsyPropertyValue(target)) {
|
|
385
|
+
assertJsonRepresentable(actual)
|
|
386
|
+
return isTruthyPropertyValue(target) === isTruthyPropertyValue(actual)
|
|
387
|
+
}
|
|
202
388
|
if (Array.isArray(target)) {
|
|
203
|
-
|
|
389
|
+
const actualString = exactMatchString(actual).toLowerCase()
|
|
390
|
+
return target.some((item) => exactMatchString(item).toLowerCase() === actualString)
|
|
204
391
|
}
|
|
205
|
-
return
|
|
392
|
+
return exactMatchString(target).toLowerCase() === exactMatchString(actual).toLowerCase()
|
|
206
393
|
}
|
|
207
394
|
|
|
208
395
|
const compare = (lhs: any, rhs: any, comparisonOperator: string): boolean => {
|
|
@@ -221,17 +408,17 @@ export function matchFeatureFlagProperty(
|
|
|
221
408
|
case 'is_set':
|
|
222
409
|
return true
|
|
223
410
|
case 'icontains':
|
|
224
|
-
return
|
|
411
|
+
return asciiLowercase(overrideValue).includes(asciiLowercase(value))
|
|
225
412
|
case 'not_icontains':
|
|
226
|
-
return !
|
|
413
|
+
return !asciiLowercase(overrideValue).includes(asciiLowercase(value))
|
|
227
414
|
case 'starts_with':
|
|
228
|
-
return
|
|
415
|
+
return asciiLowercase(overrideValue).startsWith(asciiLowercase(value))
|
|
229
416
|
case 'not_starts_with':
|
|
230
|
-
return !
|
|
417
|
+
return !asciiLowercase(overrideValue).startsWith(asciiLowercase(value))
|
|
231
418
|
case 'ends_with':
|
|
232
|
-
return
|
|
419
|
+
return asciiLowercase(overrideValue).endsWith(asciiLowercase(value))
|
|
233
420
|
case 'not_ends_with':
|
|
234
|
-
return !
|
|
421
|
+
return !asciiLowercase(overrideValue).endsWith(asciiLowercase(value))
|
|
235
422
|
case 'regex':
|
|
236
423
|
return isValidRegex(String(value)) && String(overrideValue).match(String(value)) !== null
|
|
237
424
|
case 'not_regex':
|
package/src/index.ts
CHANGED
|
@@ -42,9 +42,8 @@ export {
|
|
|
42
42
|
buildResourceAttributes,
|
|
43
43
|
getOtlpSeverityNumber,
|
|
44
44
|
getOtlpSeverityText,
|
|
45
|
-
toOtlpAnyValue,
|
|
46
|
-
toOtlpKeyValueList,
|
|
47
45
|
} from './logs/logs-utils'
|
|
46
|
+
export { toOtlpAnyValue, toOtlpKeyValueList } from './utils/otlp-any-value'
|
|
48
47
|
export { PostHogLogs } from './logs'
|
|
49
48
|
export type {
|
|
50
49
|
BeforeSendLogFn,
|