@statsig/js-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 - Precomputed 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-client).
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-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-client',
10
+ };
package/package.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "@statsig/js-client",
3
+ "version": "0.0.1-beta.10",
4
+ "dependencies": {
5
+ "@statsig/client-core": "0.0.1-beta.10"
6
+ },
7
+ "jsdelivr": "./build/js-client.min.js",
8
+ "type": "commonjs",
9
+ "main": "./src/index.js",
10
+ "typings": "./src/index.d.ts"
11
+ }
package/project.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "js-client",
3
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "packages/js-client/src",
5
+ "projectType": "library",
6
+ "targets": {
7
+ "build": {
8
+ "command": ":",
9
+ "dependsOn": ["^build", "build-webpack"]
10
+ },
11
+ "publish": {
12
+ "command": "ts-node ./tools/scripts/publish.ts js-client",
13
+ "dependsOn": ["build", "^build-webpack"]
14
+ },
15
+ "lint": {
16
+ "executor": "@nx/eslint:lint",
17
+ "outputs": ["{options.outputFile}"]
18
+ },
19
+ "test": {
20
+ "executor": "@nx/jest:jest",
21
+ "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
22
+ "options": {
23
+ "jestConfig": "packages/js-client/jest.config.ts"
24
+ }
25
+ },
26
+ "build-ts": {
27
+ "executor": "@nx/js:tsc",
28
+ "outputs": ["{options.outputPath}"],
29
+ "options": {
30
+ "outputPath": "dist/packages/js-client",
31
+ "main": "packages/js-client/src/index.ts",
32
+ "tsConfig": "packages/js-client/tsconfig.lib.json",
33
+ "assets": ["packages/js-client/*.md"]
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-client/tsconfig.lib.json",
46
+ "webpackConfig": "packages/js-client/webpack.config.js"
47
+ },
48
+ "dependsOn": ["build-ts"]
49
+ }
50
+ },
51
+ "tags": []
52
+ }
@@ -0,0 +1,19 @@
1
+ import {
2
+ DynamicConfigEvaluation,
3
+ GateEvaluation,
4
+ LayerEvaluation,
5
+ } from '@statsig/client-core';
6
+
7
+ export type EvaluationResponseWithUpdates = {
8
+ feature_gates: Record<string, GateEvaluation>;
9
+ dynamic_configs: Record<string, DynamicConfigEvaluation>;
10
+ layer_configs: Record<string, LayerEvaluation>;
11
+ time: number;
12
+ has_updates: true;
13
+ hash_used: 'none' | 'sha256' | 'djb2';
14
+ derived_fields?: Record<string, unknown>;
15
+ };
16
+
17
+ export type EvaluationResponse =
18
+ | EvaluationResponseWithUpdates
19
+ | { has_updates: false };
@@ -0,0 +1,108 @@
1
+ import { DJB2Object, typedJsonParse } from '@statsig/client-core';
2
+
3
+ import { EvaluationResponseWithUpdates } from './EvaluationData';
4
+
5
+ type DeltasEvaluationResponse = EvaluationResponseWithUpdates & {
6
+ deleted_configs?: string[];
7
+ deleted_gates?: string[];
8
+ deleted_layers?: string[];
9
+ is_delta: true;
10
+ has_updates: true;
11
+ checksum: string;
12
+ deltas_full_response?: Record<string, unknown>;
13
+ };
14
+
15
+ export type DeltasFailureInfo = {
16
+ hadBadDeltaChecksum: boolean;
17
+ badChecksum?: string;
18
+ badMergedConfigs?: Record<string, unknown>;
19
+ badFullResponse?: Record<string, unknown>;
20
+ };
21
+
22
+ type DeltasResult = string | DeltasFailureInfo | null;
23
+
24
+ export function resolveDeltasResponse(
25
+ cache: EvaluationResponseWithUpdates,
26
+ deltasString: string,
27
+ ): DeltasResult {
28
+ const deltas = typedJsonParse<DeltasEvaluationResponse>(
29
+ deltasString,
30
+ 'checksum',
31
+ 'Failed to parse DeltasEvaluationResponse',
32
+ );
33
+
34
+ if (!deltas) {
35
+ return {
36
+ hadBadDeltaChecksum: true,
37
+ };
38
+ }
39
+
40
+ const merged = _mergeDeltasIntoCache(cache, deltas);
41
+ const resolved = _handleDeletedEntries(merged);
42
+
43
+ const actualChecksum = DJB2Object({
44
+ feature_gates: resolved.feature_gates,
45
+ dynamic_configs: resolved.dynamic_configs,
46
+ layer_configs: resolved.layer_configs,
47
+ });
48
+
49
+ const isMatch = actualChecksum === deltas.checksum;
50
+ if (!isMatch) {
51
+ return {
52
+ hadBadDeltaChecksum: true,
53
+ badChecksum: actualChecksum,
54
+ badMergedConfigs: resolved,
55
+ badFullResponse: deltas.deltas_full_response,
56
+ };
57
+ }
58
+
59
+ return JSON.stringify(resolved);
60
+ }
61
+
62
+ function _mergeDeltasIntoCache(
63
+ cache: EvaluationResponseWithUpdates,
64
+ deltas: DeltasEvaluationResponse,
65
+ ): DeltasEvaluationResponse {
66
+ return {
67
+ ...cache,
68
+ ...deltas,
69
+ feature_gates: {
70
+ ...cache.feature_gates,
71
+ ...deltas.feature_gates,
72
+ },
73
+ layer_configs: {
74
+ ...cache.layer_configs,
75
+ ...deltas.layer_configs,
76
+ },
77
+ dynamic_configs: {
78
+ ...cache.dynamic_configs,
79
+ ...deltas.dynamic_configs,
80
+ },
81
+ };
82
+ }
83
+
84
+ function _handleDeletedEntries(
85
+ deltas: DeltasEvaluationResponse,
86
+ ): EvaluationResponseWithUpdates {
87
+ const result = deltas;
88
+
89
+ _deleteEntriesInRecord(deltas.deleted_gates, result.feature_gates);
90
+ delete result.deleted_gates;
91
+
92
+ _deleteEntriesInRecord(deltas.deleted_configs, result.dynamic_configs);
93
+ delete result.deleted_configs;
94
+
95
+ _deleteEntriesInRecord(deltas.deleted_layers, result.layer_configs);
96
+ delete result.deleted_layers;
97
+
98
+ return result;
99
+ }
100
+
101
+ function _deleteEntriesInRecord(
102
+ keys: string[] | undefined,
103
+ values: Record<string, unknown>,
104
+ ) {
105
+ keys?.forEach((key) => {
106
+ delete values[key];
107
+ });
108
+ }
@@ -0,0 +1,99 @@
1
+ import {
2
+ AnyEvaluation,
3
+ DataAdapterResult,
4
+ DataSource,
5
+ DetailedEvaluation,
6
+ DynamicConfigEvaluation,
7
+ EvaluationDetails,
8
+ GateEvaluation,
9
+ LayerEvaluation,
10
+ typedJsonParse,
11
+ } from '@statsig/client-core';
12
+
13
+ import { EvaluationResponse } from './EvaluationData';
14
+
15
+ type EvaluationStoreValues = EvaluationResponse & { has_updates: true };
16
+
17
+ export default class EvaluationStore {
18
+ private _values: EvaluationStoreValues | null = null;
19
+ private _source: DataSource = 'Uninitialized';
20
+ private _lcut = 0;
21
+ private _receivedAt = 0;
22
+
23
+ constructor(private _sdkKey: string) {}
24
+
25
+ reset(): void {
26
+ this._values = null;
27
+ this._source = 'Loading';
28
+ this._lcut = 0;
29
+ this._receivedAt = 0;
30
+ }
31
+
32
+ finalize(): void {
33
+ if (this._values) {
34
+ return;
35
+ }
36
+
37
+ this._source = 'NoValues';
38
+ }
39
+
40
+ setValuesFromDataAdapter(result: DataAdapterResult | null): void {
41
+ if (!result) {
42
+ return;
43
+ }
44
+
45
+ const values = typedJsonParse<EvaluationResponse>(
46
+ result.data,
47
+ 'has_updates',
48
+ 'Failed to parse EvaluationResponse',
49
+ );
50
+
51
+ if (values?.has_updates !== true) {
52
+ return;
53
+ }
54
+
55
+ this._lcut = values.time;
56
+ this._receivedAt = result.receivedAt;
57
+ this._source = result.source;
58
+ this._values = values;
59
+ }
60
+
61
+ getGate(name: string): DetailedEvaluation<GateEvaluation> {
62
+ const evaluation = this._values?.feature_gates[name] ?? null;
63
+ return this._makeDetailedEvaluation(evaluation);
64
+ }
65
+
66
+ getConfig(name: string): DetailedEvaluation<DynamicConfigEvaluation> {
67
+ const evaluation = this._values?.dynamic_configs[name] ?? null;
68
+ return this._makeDetailedEvaluation(evaluation);
69
+ }
70
+
71
+ getLayer(name: string): DetailedEvaluation<LayerEvaluation> {
72
+ const evaluation = this._values?.layer_configs[name] ?? null;
73
+ return this._makeDetailedEvaluation(evaluation);
74
+ }
75
+
76
+ private _makeDetailedEvaluation<T extends AnyEvaluation>(
77
+ evaluation: T | null,
78
+ ): DetailedEvaluation<T> {
79
+ return {
80
+ evaluation,
81
+ details: this._getDetails(evaluation == null),
82
+ };
83
+ }
84
+
85
+ private _getDetails(isUnrecognized: boolean): EvaluationDetails {
86
+ if (this._source === 'Uninitialized' || this._source === 'NoValues') {
87
+ return { reason: this._source };
88
+ }
89
+
90
+ const subreason = isUnrecognized ? 'Unrecognized' : 'Recognized';
91
+ const reason = `${this._source}:${subreason}`;
92
+
93
+ return {
94
+ reason,
95
+ lcut: this._lcut,
96
+ receivedAt: this._receivedAt,
97
+ };
98
+ }
99
+ }
package/src/Network.ts ADDED
@@ -0,0 +1,112 @@
1
+ import {
2
+ NetworkCore,
3
+ StatsigClientEmitEventFunc,
4
+ StatsigUser,
5
+ _getOverridableUrl,
6
+ typedJsonParse,
7
+ } from '@statsig/client-core';
8
+
9
+ import { EvaluationResponse } from './EvaluationData';
10
+ import { resolveDeltasResponse } from './EvaluationResponseDeltas';
11
+ import { StatsigOptions } from './StatsigOptions';
12
+
13
+ const DEFAULT_API = 'https://api.statsig.com/v1';
14
+ const DEFAULT_ENDPOINT = '/initialize';
15
+
16
+ type EvaluationsFetchArgs = {
17
+ hash: 'djb2' | 'sha256' | 'none';
18
+ deltasResponseRequested: boolean;
19
+ user?: StatsigUser;
20
+ sinceTime?: number;
21
+ previousDerivedFields?: Record<string, unknown>;
22
+ };
23
+
24
+ export default class StatsigNetwork extends NetworkCore {
25
+ private _initializeUrl: string;
26
+
27
+ constructor(
28
+ options: StatsigOptions | null,
29
+ emitter?: StatsigClientEmitEventFunc,
30
+ ) {
31
+ super(options, emitter);
32
+
33
+ this._initializeUrl = _getOverridableUrl(
34
+ options?.initializeUrl,
35
+ options?.api,
36
+ DEFAULT_ENDPOINT,
37
+ DEFAULT_API,
38
+ );
39
+ }
40
+
41
+ async fetchEvaluations(
42
+ sdkKey: string,
43
+ current: string | null,
44
+ user?: StatsigUser,
45
+ ): Promise<string | null> {
46
+ const cache = current
47
+ ? typedJsonParse<EvaluationResponse>(
48
+ current,
49
+ 'has_updates',
50
+ 'Failed to parse cached EvaluationResponse',
51
+ )
52
+ : null;
53
+
54
+ let data: EvaluationsFetchArgs = {
55
+ user,
56
+ hash: 'djb2',
57
+ deltasResponseRequested: false,
58
+ };
59
+
60
+ if (cache?.has_updates) {
61
+ data = {
62
+ ...data,
63
+ sinceTime: cache.time,
64
+ previousDerivedFields:
65
+ 'derived_fields' in cache ? cache.derived_fields : {},
66
+ deltasResponseRequested: true,
67
+ };
68
+ }
69
+
70
+ return this._fetchEvaluations(sdkKey, cache, data);
71
+ }
72
+
73
+ private async _fetchEvaluations(
74
+ sdkKey: string,
75
+ cache: EvaluationResponse | null,
76
+ data: EvaluationsFetchArgs,
77
+ ): Promise<string | null> {
78
+ const response = await this.post({
79
+ sdkKey,
80
+ url: this._initializeUrl,
81
+ data,
82
+ retries: 2,
83
+ });
84
+
85
+ if (response?.code === 204) {
86
+ return '{"has_updates": false}';
87
+ }
88
+
89
+ if (response?.code !== 200) {
90
+ return response?.body ?? null;
91
+ }
92
+
93
+ if (
94
+ cache?.has_updates !== true ||
95
+ response.body?.includes('"is_delta":true') !== true
96
+ ) {
97
+ return response.body;
98
+ }
99
+
100
+ const result = resolveDeltasResponse(cache, response.body);
101
+ if (typeof result === 'string') {
102
+ return result;
103
+ }
104
+
105
+ // retry without deltas
106
+ return this._fetchEvaluations(sdkKey, cache, {
107
+ ...data,
108
+ ...result,
109
+ deltasResponseRequested: false,
110
+ });
111
+ }
112
+ }