Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"rust-analyzer.cargo.target": "wasm32-unknown-unknown",
"rust-analyzer.cargo.features": [],
"rust-analyzer.cargo.extraEnv": {
"PROTOC":"/opt/homebrew/bin/protoc"
},
"protoc": {
"options": ["-I/${workspaceRoot}/confidence-resolver/protos"]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ message ResolverStateUriResponse {
string signed_uri = 1;
// At what time the state uri expires
google.protobuf.Timestamp expire_time = 2;
// The account the referenced state belongs to
string account = 3;
}

// Request to get the resolver state for the whole account
Expand Down
10 changes: 10 additions & 0 deletions openfeature-provider/js/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
root = true

[*]
end_of_line = lf
insert_final_newline = true

[*.{js,json,yml}]
charset = utf-8
indent_style = space
indent_size = 2
4 changes: 4 additions & 0 deletions openfeature-provider/js/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/.yarn/** linguist-vendored
/.yarn/releases/* binary
/.yarn/plugins/**/* binary
/.pnp.* binary linguist-generated
10 changes: 10 additions & 0 deletions openfeature-provider/js/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
node_modules/
src/proto/
dist/
.env.test
1 change: 1 addition & 0 deletions openfeature-provider/js/.yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nodeLinker: node-modules
1 change: 1 addition & 0 deletions openfeature-provider/js/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# js
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ask AI to fix!

54 changes: 54 additions & 0 deletions openfeature-provider/js/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "@spotify-confidence/openfeature-server-provider-local",
"version": "0.0.0",
"private": true,
"description": "Spotify Condfidence Open Feature provider",
"type": "module",
"files": [
"dist"
],
"main": "./dist/index.node.js",
"module": "./dist/index.browser.js",
"types": "./dist/index.node.d.ts",
"exports": {
".": {
"node": {
"types": "./dist/index.node.d.ts",
"default": "./dist/index.node.js"
},
"browser": {
"types": "./dist/index.browser.d.ts",
"default": "./dist/index.browser.js"
},
"default": {
"types": "./dist/index.node.d.ts",
"default": "./dist/index.node.js"
}
},
"./package.json": "./package.json"
},
"scripts": {
"build": "tsdown",
"dev": "tsdown --watch",
"test": "vitest",
"proto:gen": "rm -rf src/proto && mkdir -p src/proto && protoc --plugin=node_modules/.bin/protoc-gen-ts_proto --ts_proto_opt useOptionals=messages --ts_proto_opt esModuleInterop=true --ts_proto_out src/proto -Iproto api.proto messages.proto"
},
"dependencies": {
"loglevel": "^1.9.2"
},
"devDependencies": {
"@openfeature/core": "^1.9.0",
"@openfeature/server-sdk": "^1.19.0",
"@types/node": "^24.0.1",
"@vitest/coverage-v8": "^3.2.4",
"dotenv": "^17.2.2",
"rolldown": "1.0.0-beta.38",
"ts-proto": "^2.7.3",
"tsdown": "latest",
"vitest": "^3.2.4"
},
"peerDependencies": {
"@openfeature/server-sdk": "^1.19.0"
},
"packageManager": "[email protected]"
}
92 changes: 92 additions & 0 deletions openfeature-provider/js/proto/api.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
syntax = "proto3";

import "google/protobuf/struct.proto";
import "google/protobuf/timestamp.proto";

message ResolveFlagsRequest {
// If non-empty, the specific flags are resolved, otherwise all flags
// available to the client will be resolved.
repeated string flags = 1;

// An object that contains data used in the flag resolve. For example,
// the targeting key e.g. the id of the randomization unit, other attributes
// like country or version that are used for targeting.
google.protobuf.Struct evaluation_context = 2;

// Credentials for the client. It is used to identify the client and find
// the flags that are available to it.
string client_secret = 3;

// Determines whether the flags should be applied directly as part of the
// resolve, or delayed until `ApplyFlag` is called. A flag is typically
// applied when it is used, if this occurs much later than the resolve, then
// `apply` should likely be set to false.
bool apply = 4;

// Information about the SDK used to initiate the request.
// Sdk sdk = 5;
}

message ResolveFlagsResponse {
// The list of all flags that could be resolved. Note: if any flag was
// archived it will not be included in this list.
repeated ResolvedFlag resolved_flags = 1;

// An opaque token that is used when `apply` is set to false in `ResolveFlags`.
// When `apply` is set to false, the token must be passed to `ApplyFlags`.
bytes resolve_token = 2;

// Unique identifier for this particular resolve request.
string resolve_id = 3;
}


message ResolvedFlag {
// The id of the flag that as resolved.
string flag = 1;

// The id of the resolved variant has the format `flags/abc/variants/xyz`.
string variant = 2;

// The value corresponding to the variant. It will always be a json object,
// for example `{ "color": "red", "size": 12 }`.
google.protobuf.Struct value = 3;

// The schema of the value that was returned. For example:
// ```
// {
// "schema": {
// "color": { "stringSchema": {} },
// "size": { "intSchema": {} }
// }
// }
// ```
// types.v1.FlagSchema.StructFlagSchema flag_schema = 4;

// The reason to why the flag could be resolved or not.
ResolveReason reason = 5;
}


enum ResolveReason {
// Unspecified enum.
RESOLVE_REASON_UNSPECIFIED = 0;
// The flag was successfully resolved because one rule matched.
RESOLVE_REASON_MATCH = 1;
// The flag could not be resolved because no rule matched.
RESOLVE_REASON_NO_SEGMENT_MATCH = 2;
// The flag could not be resolved because the matching rule had no variant
// that could be assigned.
RESOLVE_REASON_NO_TREATMENT_MATCH = 3 [deprecated = true];
// The flag could not be resolved because it was archived.
RESOLVE_REASON_FLAG_ARCHIVED = 4;
// The flag could not be resolved because the targeting key field was invalid
RESOLVE_REASON_TARGETING_KEY_ERROR = 5;
// Unknown error occurred during the resolve
RESOLVE_REASON_ERROR = 6;
}

message SetResolverStateRequest {
bytes state = 1;
string account_id = 2;
}
14 changes: 14 additions & 0 deletions openfeature-provider/js/proto/messages.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
syntax = "proto3";

message Void {}

message Request {
bytes data = 1;
}

message Response {
oneof result {
bytes data = 1;
string error = 2;
}
}
100 changes: 100 additions & 0 deletions openfeature-provider/js/src/ConfidenceServerProviderLocal.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { OpenFeature } from '@openfeature/server-sdk';
import { ConfidenceServerProviderLocal } from './ConfidenceServerProviderLocal';
import { readFileSync } from 'node:fs';
import { WasmResolver } from './WasmResolver';
// import log from 'loglevel';
// log.setLevel("debug");

const {
CONFIDENCE_API_CLIENT_ID,
CONFIDENCE_API_CLIENT_SECRET,
} = requireEnv('CONFIDENCE_API_CLIENT_ID', 'CONFIDENCE_API_CLIENT_SECRET');

const moduleBytes = readFileSync(__dirname + '/../../../wasm/confidence_resolver.wasm');
const module = new WebAssembly.Module(moduleBytes);
const resolver = await WasmResolver.load(module);
const confidenceProvider = new ConfidenceServerProviderLocal(resolver, {
flagClientSecret: 'RxDVTrXvc6op1XxiQ4OaR31dKbJ39aYV',
apiClientId: CONFIDENCE_API_CLIENT_ID,
apiClientSecret: CONFIDENCE_API_CLIENT_SECRET
});

describe('ConfidenceServerProvider E2E tests', () => {
beforeAll( async () => {

await OpenFeature.setProviderAndWait(confidenceProvider);
OpenFeature.setContext({
targetingKey: 'test-a', // control
});
});

afterAll(() => OpenFeature.close())

it('should resolve a boolean e2e', async () => {
const client = OpenFeature.getClient();

expect(await client.getBooleanValue('web-sdk-e2e-flag.bool', true)).toBeFalsy();
});

it('should resolve an int', async () => {
const client = OpenFeature.getClient();

expect(await client.getNumberValue('web-sdk-e2e-flag.int', 10)).toEqual(3);
});

it('should resolve a double', async () => {
const client = OpenFeature.getClient();

expect(await client.getNumberValue('web-sdk-e2e-flag.double', 10)).toEqual(3.5);
});

it('should resolve a string', async () => {
const client = OpenFeature.getClient();

expect(await client.getStringValue('web-sdk-e2e-flag.str', 'default')).toEqual('control');
});

it('should resolve a struct', async () => {
const client = OpenFeature.getClient();
const expectedObject = {
int: 4,
str: 'obj control',
bool: false,
double: 3.6,
['obj-obj']: {},
};

expect(await client.getObjectValue('web-sdk-e2e-flag.obj', {})).toEqual(expectedObject);
});

it('should resolve a sub value from a struct', async () => {
const client = OpenFeature.getClient();

expect(await client.getBooleanValue('web-sdk-e2e-flag.obj.bool', true)).toBeFalsy();
});

it('should resolve a sub value from a struct with details with resolve token for client side apply call', async () => {
const client = OpenFeature.getClient();
const expectedObject = {
flagKey: 'web-sdk-e2e-flag.obj.double',
reason: 'MATCH',
variant: 'flags/web-sdk-e2e-flag/variants/control',
flagMetadata: {},
value: 3.6,
};

expect(await client.getNumberDetails('web-sdk-e2e-flag.obj.double', 1)).toEqual(expectedObject);
});
});

function requireEnv<const N extends string[]>(...names:N): Record<N[number],string> {
return names.reduce((acc, name) => {
const value = process.env[name];
if(!value) throw new Error(`Missing environment variable ${name}`)
return {
...acc,
[name]: value
};
}, {}) as Record<N[number],string>;
}
Loading
Loading