@statsig/js-on-device-eval-client 0.0.1-beta.10

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/.eslintrc.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "extends": ["../../.eslintrc.json"],
3
+ "ignorePatterns": ["!**/*"],
4
+ "overrides": [
5
+ {
6
+ "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
7
+ "rules": {}
8
+ },
9
+ {
10
+ "files": ["*.ts", "*.tsx"],
11
+ "rules": {}
12
+ },
13
+ {
14
+ "files": ["*.js", "*.jsx"],
15
+ "rules": {}
16
+ },
17
+ {
18
+ "files": ["*.json"],
19
+ "parser": "jsonc-eslint-parser",
20
+ "rules": {
21
+ "@nx/dependency-checks": [
22
+ "error",
23
+ {
24
+ "ignoredDependencies": [
25
+ "jest-fetch-mock",
26
+ "@nx/webpack",
27
+ "statsig-test-helpers"
28
+ ]
29
+ }
30
+ ]
31
+ }
32
+ }
33
+ ]
34
+ }
package/README.md ADDED
@@ -0,0 +1,12 @@
1
+ # Statsig - On Device Evaluations
2
+
3
+ > [!IMPORTANT]
4
+ > This version of the SDK is still in beta. The non-beta version can be found [here](https://github.com/statsig-io/js-local-eval).
5
+
6
+ The JavaScript SDK for single user client environments. If you need a SDK for another language or server environment, check out our [other SDKs](https://docs.statsig.com/#sdks).
7
+
8
+ Statsig helps you move faster with feature gates (feature flags), and/or dynamic configs. It also allows you to run A/B/n tests to validate your new features and understand their impact on your KPIs. If you're new to Statsig, check out our product and create an account at [statsig.com](https://www.statsig.com).
9
+
10
+ ## Getting Started
11
+
12
+ Check out our [SDK docs](https://docs.statsig.com/client/javascript-sdk) to get started.
package/jest.config.ts ADDED
@@ -0,0 +1,10 @@
1
+ /* eslint-disable */
2
+ export default {
3
+ displayName: 'js-on-device-eval-client',
4
+ preset: '../../jest.preset.js',
5
+ transform: {
6
+ '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
7
+ },
8
+ moduleFileExtensions: ['ts', 'js', 'html'],
9
+ coverageDirectory: '../../coverage/packages/js-on-device-eval-client',
10
+ };
package/package.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@statsig/js-on-device-eval-client",
3
+ "version": "0.0.1-beta.10",
4
+ "dependencies": {
5
+ "@statsig/client-core": "0.0.1-beta.10",
6
+ "@statsig/sha256": "0.0.1-beta.10"
7
+ },
8
+ "jsdelivr": "./build/js-on-device-eval-client.min.js",
9
+ "type": "commonjs",
10
+ "main": "./src/index.js",
11
+ "typings": "./src/index.d.ts"
12
+ }
package/project.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "js-on-device-eval-client",
3
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "packages/js-on-device-eval-client/src",
5
+ "projectType": "library",
6
+ "targets": {
7
+ "build": {
8
+ "command": ":",
9
+ "dependsOn": ["^build", "build-webpack"]
10
+ },
11
+ "build-ts": {
12
+ "executor": "@nx/js:tsc",
13
+ "outputs": ["{options.outputPath}"],
14
+ "options": {
15
+ "outputPath": "dist/packages/js-on-device-eval-client",
16
+ "main": "packages/js-on-device-eval-client/src/index.ts",
17
+ "tsConfig": "packages/js-on-device-eval-client/tsconfig.lib.json",
18
+ "assets": ["packages/js-on-device-eval-client/*.md"]
19
+ }
20
+ },
21
+ "publish": {
22
+ "command": "ts-node ./tools/scripts/publish.ts js-on-device-eval-client",
23
+ "dependsOn": ["build"]
24
+ },
25
+ "lint": {
26
+ "executor": "@nx/eslint:lint",
27
+ "outputs": ["{options.outputFile}"]
28
+ },
29
+ "test": {
30
+ "executor": "@nx/jest:jest",
31
+ "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
32
+ "options": {
33
+ "jestConfig": "packages/js-on-device-eval-client/jest.config.ts"
34
+ }
35
+ },
36
+ "build-webpack": {
37
+ "executor": "@nx/webpack:webpack",
38
+ "outputs": ["{options.outputPath}"],
39
+ "defaultConfiguration": "production",
40
+ "options": {
41
+ "statsJson": true,
42
+ "verbose": true,
43
+ "outputPath": "overridden in webpack config",
44
+ "main": "overridden in webpack config",
45
+ "tsConfig": "packages/js-on-device-eval-client/tsconfig.lib.json",
46
+ "webpackConfig": "packages/js-on-device-eval-client/webpack.config.js"
47
+ },
48
+ "dependsOn": ["build-ts"]
49
+ }
50
+ },
51
+ "tags": []
52
+ }
package/src/Errors.ts ADDED
@@ -0,0 +1,6 @@
1
+ export class StatsigUnsupportedEvaluationError extends Error {
2
+ constructor(condition?: string) {
3
+ super(`Unsupported condition or operator: ${condition}`);
4
+ Object.setPrototypeOf(this, StatsigUnsupportedEvaluationError.prototype);
5
+ }
6
+ }
@@ -0,0 +1,189 @@
1
+ export default {
2
+ compareNumbers(left: unknown, right: unknown, operator: string): boolean {
3
+ if (left == null || right == null) {
4
+ return false;
5
+ }
6
+
7
+ const numA = Number(left);
8
+ const numB = Number(right);
9
+ if (isNaN(numA) || isNaN(numB)) {
10
+ return false;
11
+ }
12
+
13
+ switch (operator) {
14
+ case 'gt':
15
+ return left > right;
16
+ case 'gte':
17
+ return left >= right;
18
+ case 'lt':
19
+ return left < right;
20
+ case 'lte':
21
+ return left <= right;
22
+ default:
23
+ return false;
24
+ }
25
+ },
26
+
27
+ compareVersions(left: unknown, right: unknown, operator: string): boolean {
28
+ if (left == null || right == null) {
29
+ return false;
30
+ }
31
+
32
+ let leftStr = String(left);
33
+ let rightStr = String(right);
34
+
35
+ const removeSuffix = (str: string): string => {
36
+ const index = str.indexOf('-');
37
+ return index !== -1 ? str.substring(0, index) : str;
38
+ };
39
+
40
+ leftStr = removeSuffix(leftStr);
41
+ rightStr = removeSuffix(rightStr);
42
+
43
+ const comparison = (leftStr: string, rightStr: string): number => {
44
+ const leftParts = leftStr.split('.').map((part) => parseInt(part));
45
+ const rightParts = rightStr.split('.').map((part) => parseInt(part));
46
+
47
+ let i = 0;
48
+ while (i < Math.max(leftParts.length, rightParts.length)) {
49
+ const leftCount = i < leftParts.length ? leftParts[i] : 0;
50
+ const rightCount = i < rightParts.length ? rightParts[i] : 0;
51
+
52
+ if (leftCount < rightCount) {
53
+ return -1;
54
+ }
55
+
56
+ if (leftCount > rightCount) {
57
+ return 1;
58
+ }
59
+
60
+ i++;
61
+ }
62
+ return 0;
63
+ };
64
+
65
+ const result = comparison(leftStr, rightStr);
66
+ switch (operator) {
67
+ case 'version_gt':
68
+ return result > 0;
69
+ case 'version_gte':
70
+ return result >= 0;
71
+ case 'version_lt':
72
+ return result < 0;
73
+ case 'version_lte':
74
+ return result <= 0;
75
+ case 'version_eq':
76
+ return result === 0;
77
+ case 'version_neq':
78
+ return result !== 0;
79
+ default:
80
+ return false;
81
+ }
82
+ },
83
+
84
+ compareStringInArray(
85
+ value: unknown,
86
+ array: unknown,
87
+ operator: string,
88
+ ): boolean {
89
+ if (!Array.isArray(array)) {
90
+ return false;
91
+ }
92
+
93
+ const ignoreCase =
94
+ operator !== 'any_case_sensitive' && operator !== 'none_case_sensitive';
95
+
96
+ const result =
97
+ array.findIndex((current) => {
98
+ const valueString = String(value);
99
+ const currentString = String(current);
100
+
101
+ const left = ignoreCase ? valueString.toLowerCase() : valueString;
102
+ const right = ignoreCase ? currentString.toLowerCase() : currentString;
103
+
104
+ switch (operator) {
105
+ case 'any':
106
+ case 'none':
107
+ case 'any_case_sensitive':
108
+ case 'none_case_sensitive':
109
+ return left === right;
110
+ case 'str_starts_with_any':
111
+ return left.startsWith(right);
112
+ case 'str_ends_with_any':
113
+ return left.endsWith(right);
114
+ case 'str_contains_any':
115
+ case 'str_contains_none':
116
+ return left.includes(right);
117
+ default:
118
+ return false;
119
+ }
120
+ }) !== -1;
121
+
122
+ switch (operator) {
123
+ case 'none':
124
+ case 'none_case_sensitive':
125
+ case 'str_contains_none':
126
+ return !result;
127
+ default:
128
+ return result;
129
+ }
130
+ },
131
+
132
+ compareStringWithRegEx(value: unknown, target: unknown): boolean {
133
+ try {
134
+ const valueString = String(value);
135
+ if (valueString.length < 1000) {
136
+ return new RegExp(String(target)).test(valueString);
137
+ }
138
+ } catch (e) {
139
+ // noop
140
+ }
141
+
142
+ return false;
143
+ },
144
+
145
+ compareTime(left: unknown, right: unknown, operator: string): boolean {
146
+ if (left == null || right == null) {
147
+ return false;
148
+ }
149
+
150
+ try {
151
+ // Try to parse into date as a string first, if not, try unixtime
152
+ let dateLeft = new Date(String(left));
153
+ if (isNaN(dateLeft.getTime())) {
154
+ dateLeft = new Date(Number(left));
155
+ }
156
+
157
+ let dateRight = new Date(String(right));
158
+ if (isNaN(dateRight.getTime())) {
159
+ dateRight = new Date(Number(right));
160
+ }
161
+
162
+ const timeLeft = dateLeft.getTime();
163
+ const timeRight = dateRight.getTime();
164
+
165
+ if (isNaN(timeLeft) || isNaN(timeRight)) {
166
+ return false;
167
+ }
168
+
169
+ switch (operator) {
170
+ case 'before':
171
+ return timeLeft < timeRight;
172
+ case 'after':
173
+ return timeLeft > timeRight;
174
+ case 'on':
175
+ return _startOfDay(dateLeft) === _startOfDay(dateRight);
176
+ default:
177
+ return false;
178
+ }
179
+ } catch (e) {
180
+ // malformatted input, returning false
181
+ return false;
182
+ }
183
+ },
184
+ };
185
+
186
+ function _startOfDay(date: Date): number {
187
+ date.setUTCHours(0, 0, 0, 0);
188
+ return date.getTime();
189
+ }
@@ -0,0 +1,92 @@
1
+ import {
2
+ DynamicConfigEvaluation,
3
+ GateEvaluation,
4
+ LayerEvaluation,
5
+ SecondaryExposure,
6
+ } from '@statsig/client-core';
7
+
8
+ import { Spec } from './SpecStore';
9
+
10
+ export type EvaluationResult = {
11
+ readonly unsupported: boolean;
12
+ readonly bool_value: boolean;
13
+ readonly rule_id: string;
14
+ readonly secondary_exposures: SecondaryExposure[];
15
+ readonly json_value: Record<string, unknown>;
16
+ readonly explicit_parameters: string[] | null;
17
+ readonly allocated_experiment_name: string | null;
18
+ readonly undelegated_secondary_exposures: SecondaryExposure[] | undefined;
19
+ readonly is_experiment_group: boolean;
20
+ readonly group_name: string | null;
21
+ };
22
+
23
+ export function makeEvalResult(
24
+ overrides: Partial<EvaluationResult>,
25
+ ): EvaluationResult {
26
+ const base: EvaluationResult = {
27
+ unsupported: false,
28
+ bool_value: false,
29
+ rule_id: '',
30
+ secondary_exposures: [],
31
+ json_value: {},
32
+ explicit_parameters: null,
33
+ allocated_experiment_name: null,
34
+ is_experiment_group: false,
35
+ group_name: null,
36
+ undelegated_secondary_exposures: undefined,
37
+ };
38
+
39
+ return { ...base, ...overrides };
40
+ }
41
+
42
+ export function resultToGateEval(
43
+ spec: Spec,
44
+ result: EvaluationResult,
45
+ ): GateEvaluation {
46
+ return {
47
+ name: spec.name,
48
+ id_type: spec.idType,
49
+ rule_id: result.rule_id,
50
+ value: result.bool_value,
51
+ secondary_exposures: result.secondary_exposures,
52
+ };
53
+ }
54
+
55
+ export function resultToConfigEval(
56
+ spec: Spec,
57
+ result: EvaluationResult,
58
+ ): DynamicConfigEvaluation {
59
+ return {
60
+ name: spec.name,
61
+ id_type: spec.idType,
62
+ rule_id: result.rule_id,
63
+ value: result.json_value,
64
+ secondary_exposures: result.secondary_exposures,
65
+ group: result.group_name ?? '',
66
+ group_name: result.group_name ?? undefined,
67
+ is_device_based: false,
68
+ is_experiment_active: spec.isActive,
69
+ is_user_in_experiment: result.is_experiment_group,
70
+ };
71
+ }
72
+
73
+ export function resultToLayerEval(
74
+ layerSpec: Spec,
75
+ experimentSpec: Spec | null,
76
+ result: EvaluationResult,
77
+ ): LayerEvaluation {
78
+ return {
79
+ name: layerSpec.name,
80
+ rule_id: result.rule_id,
81
+ value: result.json_value,
82
+ secondary_exposures: result.secondary_exposures,
83
+ undelegated_secondary_exposures: result.undelegated_secondary_exposures,
84
+ allocated_experiment_name: result.allocated_experiment_name ?? '',
85
+ explicit_parameters: result.explicit_parameters ?? [],
86
+ group: result.group_name ?? '',
87
+ group_name: result.group_name ?? undefined,
88
+ is_device_based: false,
89
+ is_experiment_active: experimentSpec?.isActive,
90
+ is_user_in_experiment: result.is_experiment_group,
91
+ };
92
+ }