@sessionsight/flags 1.0.0 → 1.0.1

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.
@@ -0,0 +1,19 @@
1
+ import type { FeatureFlagConfig, FlagEvaluationContext, FlagListResult } from './types.js';
2
+ export declare class FeatureFlagClient {
3
+ private apiUrl;
4
+ private secretApiKey;
5
+ private propertyId;
6
+ private environment;
7
+ private flags;
8
+ private context;
9
+ private initialized;
10
+ constructor(config: FeatureFlagConfig);
11
+ init(context?: FlagEvaluationContext): Promise<void>;
12
+ getBooleanFlag(key: string, defaultValue: boolean): boolean;
13
+ getStringFlag(key: string, defaultValue: string): string;
14
+ refresh(context?: FlagEvaluationContext): Promise<void>;
15
+ getFlags(): Promise<FlagListResult>;
16
+ destroy(): void;
17
+ isInitialized(): boolean;
18
+ private fetchFlags;
19
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/iife.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import { FeatureFlagClient } from './client.js';
2
+ import type { FeatureFlagConfig, FlagEvaluationContext } from './types.js';
3
+ export { FeatureFlagClient };
4
+ export type { FeatureFlagConfig, FlagEvaluationContext, EvaluatedFlag, EvaluatedFlags } from './types.js';
5
+ declare const FeatureFlags: {
6
+ init(config: FeatureFlagConfig, context?: FlagEvaluationContext): Promise<void>;
7
+ getBooleanFlag(key: string, defaultValue: boolean): boolean;
8
+ getStringFlag(key: string, defaultValue: string): string;
9
+ refresh(context?: FlagEvaluationContext): Promise<void>;
10
+ destroy(): void;
11
+ };
12
+ export default FeatureFlags;
package/dist/index.js ADDED
@@ -0,0 +1,167 @@
1
+ // ../sdk-shared/src/index.ts
2
+ var DEFAULT_API_URL = "https://api.sessionsight.com";
3
+ function normalizeApiUrl(url) {
4
+ const normalized = (url || DEFAULT_API_URL).replace(/\/$/, "");
5
+ if (normalized.startsWith("http://") && !normalized.startsWith("http://localhost")) {
6
+ console.warn("SessionSight: API URL uses http:// instead of https://. Data will be transmitted unencrypted.");
7
+ }
8
+ return normalized;
9
+ }
10
+ var VISITOR_COOKIE_MAX_AGE = 365 * 24 * 60 * 60;
11
+ var registry = new Map;
12
+ function setRegistryValue(key, value) {
13
+ registry.set(key, value);
14
+ }
15
+
16
+ // src/client.ts
17
+ var FETCH_TIMEOUT_MS = 1e4;
18
+ function fetchWithTimeout(url, options) {
19
+ const controller = new AbortController;
20
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
21
+ return fetch(url, { ...options, signal: controller.signal }).finally(() => clearTimeout(timer));
22
+ }
23
+
24
+ class FeatureFlagClient {
25
+ apiUrl;
26
+ secretApiKey;
27
+ propertyId;
28
+ environment;
29
+ flags = {};
30
+ context = {};
31
+ initialized = false;
32
+ constructor(config) {
33
+ if (typeof window !== "undefined" && !("process" in globalThis)) {
34
+ throw new Error("@sessionsight/flags is a server-side SDK and cannot be used in the browser.");
35
+ }
36
+ if (!config.secretApiKey?.trim())
37
+ throw new Error("@sessionsight/flags: secretApiKey is required.");
38
+ if (!config.propertyId?.trim())
39
+ throw new Error("@sessionsight/flags: propertyId is required.");
40
+ if (!config.environment?.trim())
41
+ throw new Error("@sessionsight/flags: environment is required.");
42
+ this.secretApiKey = config.secretApiKey;
43
+ this.propertyId = config.propertyId;
44
+ this.environment = config.environment;
45
+ this.apiUrl = normalizeApiUrl(config.apiUrl || "");
46
+ }
47
+ async init(context) {
48
+ if (context)
49
+ this.context = context;
50
+ await this.fetchFlags();
51
+ this.initialized = true;
52
+ }
53
+ getBooleanFlag(key, defaultValue) {
54
+ const flag = this.flags[key];
55
+ if (!flag || flag.type !== "boolean")
56
+ return defaultValue;
57
+ return typeof flag.value === "boolean" ? flag.value : defaultValue;
58
+ }
59
+ getStringFlag(key, defaultValue) {
60
+ const flag = this.flags[key];
61
+ if (!flag || flag.type !== "string")
62
+ return defaultValue;
63
+ return typeof flag.value === "string" ? flag.value : defaultValue;
64
+ }
65
+ async refresh(context) {
66
+ if (context)
67
+ this.context = { ...this.context, ...context };
68
+ await this.fetchFlags();
69
+ }
70
+ async getFlags() {
71
+ try {
72
+ const res = await fetchWithTimeout(`${this.apiUrl}/v1/flags/list?propertyId=${encodeURIComponent(this.propertyId)}`, {
73
+ method: "GET",
74
+ headers: { "x-api-key": this.secretApiKey }
75
+ });
76
+ if (!res.ok) {
77
+ console.warn(`[SessionSight Flags] Failed to list flags: ${res.status}`);
78
+ return { flags: [] };
79
+ }
80
+ const data = await res.json();
81
+ return { flags: data.flags || [] };
82
+ } catch (err) {
83
+ console.warn("[SessionSight Flags] Failed to list flags:", err);
84
+ return { flags: [] };
85
+ }
86
+ }
87
+ destroy() {
88
+ this.flags = {};
89
+ this.context = {};
90
+ this.initialized = false;
91
+ }
92
+ isInitialized() {
93
+ return this.initialized;
94
+ }
95
+ async fetchFlags() {
96
+ try {
97
+ const res = await fetchWithTimeout(`${this.apiUrl}/v1/flags/evaluate`, {
98
+ method: "POST",
99
+ headers: {
100
+ "Content-Type": "application/json",
101
+ "x-api-key": this.secretApiKey
102
+ },
103
+ body: JSON.stringify({
104
+ propertyId: this.propertyId,
105
+ environment: this.environment,
106
+ context: this.context
107
+ })
108
+ });
109
+ if (!res.ok) {
110
+ console.warn(`[SessionSight Flags] Failed to fetch flags: ${res.status}`);
111
+ return;
112
+ }
113
+ const data = await res.json();
114
+ this.flags = data.flags || {};
115
+ if (data.evaluationToken) {
116
+ setRegistryValue("flagEvaluationToken", data.evaluationToken);
117
+ }
118
+ } catch (err) {
119
+ console.warn("[SessionSight Flags] Failed to fetch flags:", err);
120
+ }
121
+ }
122
+ }
123
+
124
+ // src/index.ts
125
+ var instance = null;
126
+ var FeatureFlags = {
127
+ async init(config, context) {
128
+ if (instance) {
129
+ console.warn("[SessionSight Flags] Already initialized. Call destroy() first.");
130
+ return;
131
+ }
132
+ instance = new FeatureFlagClient(config);
133
+ await instance.init(context);
134
+ },
135
+ getBooleanFlag(key, defaultValue) {
136
+ if (!instance) {
137
+ console.warn("[SessionSight Flags] Not initialized. Call init() first.");
138
+ return defaultValue;
139
+ }
140
+ return instance.getBooleanFlag(key, defaultValue);
141
+ },
142
+ getStringFlag(key, defaultValue) {
143
+ if (!instance) {
144
+ console.warn("[SessionSight Flags] Not initialized. Call init() first.");
145
+ return defaultValue;
146
+ }
147
+ return instance.getStringFlag(key, defaultValue);
148
+ },
149
+ async refresh(context) {
150
+ if (!instance) {
151
+ console.warn("[SessionSight Flags] Not initialized. Call init() first.");
152
+ return;
153
+ }
154
+ await instance.refresh(context);
155
+ },
156
+ destroy() {
157
+ if (instance) {
158
+ instance.destroy();
159
+ instance = null;
160
+ }
161
+ }
162
+ };
163
+ var src_default = FeatureFlags;
164
+ export {
165
+ src_default as default,
166
+ FeatureFlagClient
167
+ };
@@ -0,0 +1 @@
1
+ (()=>{class n{apiUrl;sdkKey;environment;flags={};context={};initialized=!1;constructor(t){this.sdkKey=t.sdkKey,this.environment=t.environment,this.apiUrl=(t.apiUrl||"https://api.sessionsight.com").replace(/\/$/,"")}async init(t){if(t)this.context=t;await this.fetchFlags(),this.initialized=!0}getBooleanFlag(t,i){let a=this.flags[t];if(!a||a.type!=="boolean")return i;return typeof a.value==="boolean"?a.value:i}getStringFlag(t,i){let a=this.flags[t];if(!a||a.type!=="string")return i;return typeof a.value==="string"?a.value:i}async refresh(t){if(t)this.context=t;await this.fetchFlags()}destroy(){this.flags={},this.context={},this.initialized=!1}isInitialized(){return this.initialized}async fetchFlags(){try{let t=await fetch(`${this.apiUrl}/api/flags/evaluate`,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":this.sdkKey},body:JSON.stringify({environment:this.environment,context:this.context})});if(!t.ok){console.warn(`[SessionSight Flags] Failed to fetch flags: ${t.status}`);return}let i=await t.json();this.flags=i.flags||{}}catch(t){console.warn("[SessionSight Flags] Failed to fetch flags:",t)}}}var e=null,o={async init(t,i){e=new n(t),await e.init(i)},getBooleanFlag(t,i){if(!e)return console.warn("[SessionSight Flags] Not initialized. Call init() first."),i;return e.getBooleanFlag(t,i)},getStringFlag(t,i){if(!e)return console.warn("[SessionSight Flags] Not initialized. Call init() first."),i;return e.getStringFlag(t,i)},async refresh(t){if(!e){console.warn("[SessionSight Flags] Not initialized. Call init() first.");return}await e.refresh(t)},destroy(){if(e)e.destroy(),e=null}},s=o;window.SessionSightFlags=s;})();
@@ -0,0 +1,31 @@
1
+ export interface FeatureFlagConfig {
2
+ secretApiKey: string;
3
+ propertyId: string;
4
+ environment: string;
5
+ apiUrl?: string;
6
+ }
7
+ export interface FlagEvaluationContext {
8
+ userId?: string;
9
+ /** Set visitorId to enable segment-based targeting via segment_match rules */
10
+ visitorId?: string;
11
+ [key: string]: string | number | boolean | undefined;
12
+ }
13
+ export interface EvaluatedFlag {
14
+ value: string | boolean;
15
+ type: 'boolean' | 'string';
16
+ }
17
+ export interface EvaluatedFlags {
18
+ [flagKey: string]: EvaluatedFlag;
19
+ }
20
+ export interface FlagDefinition {
21
+ id: string;
22
+ key: string;
23
+ name: string;
24
+ description?: string;
25
+ type: 'boolean' | 'string';
26
+ defaultValue: string | boolean;
27
+ createdAt: number;
28
+ }
29
+ export interface FlagListResult {
30
+ flags: FlagDefinition[];
31
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sessionsight/flags",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Feature flags SDK for SessionSight.",
5
5
  "author": "SessionSight",
6
6
  "license": "MIT",
@@ -18,6 +18,9 @@
18
18
  "feature-toggles",
19
19
  "sessionsight"
20
20
  ],
21
+ "files": [
22
+ "dist"
23
+ ],
21
24
  "type": "module",
22
25
  "main": "./src/index.ts",
23
26
  "types": "./src/index.ts",
package/src/client.ts DELETED
@@ -1,118 +0,0 @@
1
- import type { FeatureFlagConfig, FlagEvaluationContext, EvaluatedFlags, FlagListResult } from './types.js';
2
- import { normalizeApiUrl, setRegistryValue } from '@sessionsight/sdk-shared';
3
-
4
- const FETCH_TIMEOUT_MS = 10_000;
5
-
6
- function fetchWithTimeout(url: string, options: RequestInit): Promise<Response> {
7
- const controller = new AbortController();
8
- const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
9
- return fetch(url, { ...options, signal: controller.signal }).finally(() => clearTimeout(timer));
10
- }
11
-
12
- export class FeatureFlagClient {
13
- private apiUrl: string;
14
- private secretApiKey: string;
15
- private propertyId: string;
16
- private environment: string;
17
- private flags: EvaluatedFlags = {};
18
- private context: FlagEvaluationContext = {};
19
- private initialized = false;
20
-
21
- constructor(config: FeatureFlagConfig) {
22
- if (typeof window !== 'undefined' && !('process' in globalThis)) {
23
- throw new Error('@sessionsight/flags is a server-side SDK and cannot be used in the browser.');
24
- }
25
- if (!config.secretApiKey?.trim()) throw new Error('@sessionsight/flags: secretApiKey is required.');
26
- if (!config.propertyId?.trim()) throw new Error('@sessionsight/flags: propertyId is required.');
27
- if (!config.environment?.trim()) throw new Error('@sessionsight/flags: environment is required.');
28
- this.secretApiKey = config.secretApiKey;
29
- this.propertyId = config.propertyId;
30
- this.environment = config.environment;
31
- this.apiUrl = normalizeApiUrl(config.apiUrl || '');
32
- }
33
-
34
- async init(context?: FlagEvaluationContext): Promise<void> {
35
- if (context) this.context = context;
36
- await this.fetchFlags();
37
- this.initialized = true;
38
- }
39
-
40
- getBooleanFlag(key: string, defaultValue: boolean): boolean {
41
- const flag = this.flags[key];
42
- if (!flag || flag.type !== 'boolean') return defaultValue;
43
- return typeof flag.value === 'boolean' ? flag.value : defaultValue;
44
- }
45
-
46
- getStringFlag(key: string, defaultValue: string): string {
47
- const flag = this.flags[key];
48
- if (!flag || flag.type !== 'string') return defaultValue;
49
- return typeof flag.value === 'string' ? flag.value : defaultValue;
50
- }
51
-
52
- async refresh(context?: FlagEvaluationContext): Promise<void> {
53
- if (context) this.context = { ...this.context, ...context };
54
- await this.fetchFlags();
55
- }
56
-
57
- async getFlags(): Promise<FlagListResult> {
58
- try {
59
- const res = await fetchWithTimeout(`${this.apiUrl}/v1/flags/list?propertyId=${encodeURIComponent(this.propertyId)}`, {
60
- method: 'GET',
61
- headers: { 'x-api-key': this.secretApiKey },
62
- });
63
-
64
- if (!res.ok) {
65
- console.warn(`[SessionSight Flags] Failed to list flags: ${res.status}`);
66
- return { flags: [] };
67
- }
68
-
69
- const data = await res.json();
70
- return { flags: data.flags || [] };
71
- } catch (err) {
72
- console.warn('[SessionSight Flags] Failed to list flags:', err);
73
- return { flags: [] };
74
- }
75
- }
76
-
77
- destroy(): void {
78
- this.flags = {};
79
- this.context = {};
80
- this.initialized = false;
81
- }
82
-
83
- isInitialized(): boolean {
84
- return this.initialized;
85
- }
86
-
87
- private async fetchFlags(): Promise<void> {
88
- try {
89
- const res = await fetchWithTimeout(`${this.apiUrl}/v1/flags/evaluate`, {
90
- method: 'POST',
91
- headers: {
92
- 'Content-Type': 'application/json',
93
- 'x-api-key': this.secretApiKey,
94
- },
95
- body: JSON.stringify({
96
- propertyId: this.propertyId,
97
- environment: this.environment,
98
- context: this.context,
99
- }),
100
- });
101
-
102
- if (!res.ok) {
103
- console.warn(`[SessionSight Flags] Failed to fetch flags: ${res.status}`);
104
- return;
105
- }
106
-
107
- const data = await res.json();
108
- this.flags = data.flags || {};
109
-
110
- // Write opaque evaluation token to cross-SDK registry for insights SDK to pick up
111
- if (data.evaluationToken) {
112
- setRegistryValue('flagEvaluationToken', data.evaluationToken);
113
- }
114
- } catch (err) {
115
- console.warn('[SessionSight Flags] Failed to fetch flags:', err);
116
- }
117
- }
118
- }
package/src/index.ts DELETED
@@ -1,51 +0,0 @@
1
- import { FeatureFlagClient } from './client.js';
2
- import type { FeatureFlagConfig, FlagEvaluationContext } from './types.js';
3
-
4
- export { FeatureFlagClient };
5
- export type { FeatureFlagConfig, FlagEvaluationContext, EvaluatedFlag, EvaluatedFlags } from './types.js';
6
-
7
- let instance: FeatureFlagClient | null = null;
8
-
9
- const FeatureFlags = {
10
- async init(config: FeatureFlagConfig, context?: FlagEvaluationContext): Promise<void> {
11
- if (instance) {
12
- console.warn('[SessionSight Flags] Already initialized. Call destroy() first.');
13
- return;
14
- }
15
- instance = new FeatureFlagClient(config);
16
- await instance.init(context);
17
- },
18
-
19
- getBooleanFlag(key: string, defaultValue: boolean): boolean {
20
- if (!instance) {
21
- console.warn('[SessionSight Flags] Not initialized. Call init() first.');
22
- return defaultValue;
23
- }
24
- return instance.getBooleanFlag(key, defaultValue);
25
- },
26
-
27
- getStringFlag(key: string, defaultValue: string): string {
28
- if (!instance) {
29
- console.warn('[SessionSight Flags] Not initialized. Call init() first.');
30
- return defaultValue;
31
- }
32
- return instance.getStringFlag(key, defaultValue);
33
- },
34
-
35
- async refresh(context?: FlagEvaluationContext): Promise<void> {
36
- if (!instance) {
37
- console.warn('[SessionSight Flags] Not initialized. Call init() first.');
38
- return;
39
- }
40
- await instance.refresh(context);
41
- },
42
-
43
- destroy(): void {
44
- if (instance) {
45
- instance.destroy();
46
- instance = null;
47
- }
48
- },
49
- };
50
-
51
- export default FeatureFlags;
package/src/types.ts DELETED
@@ -1,36 +0,0 @@
1
- export interface FeatureFlagConfig {
2
- secretApiKey: string;
3
- propertyId: string;
4
- environment: string;
5
- apiUrl?: string;
6
- }
7
-
8
- export interface FlagEvaluationContext {
9
- userId?: string;
10
- /** Set visitorId to enable segment-based targeting via segment_match rules */
11
- visitorId?: string;
12
- [key: string]: string | number | boolean | undefined;
13
- }
14
-
15
- export interface EvaluatedFlag {
16
- value: string | boolean;
17
- type: 'boolean' | 'string';
18
- }
19
-
20
- export interface EvaluatedFlags {
21
- [flagKey: string]: EvaluatedFlag;
22
- }
23
-
24
- export interface FlagDefinition {
25
- id: string;
26
- key: string;
27
- name: string;
28
- description?: string;
29
- type: 'boolean' | 'string';
30
- defaultValue: string | boolean;
31
- createdAt: number;
32
- }
33
-
34
- export interface FlagListResult {
35
- flags: FlagDefinition[];
36
- }
@@ -1,215 +0,0 @@
1
- import { test, expect, describe, beforeEach, mock } from 'bun:test';
2
- import { FeatureFlagClient } from '../src/client';
3
-
4
- // Mock global fetch
5
- const mockFetch = mock(() => Promise.resolve(new Response()));
6
-
7
- globalThis.fetch = mockFetch as any;
8
-
9
- function mockFetchResponse(data: any, status = 200) {
10
- mockFetch.mockResolvedValue(new Response(JSON.stringify(data), {
11
- status,
12
- headers: { 'Content-Type': 'application/json' },
13
- }));
14
- }
15
-
16
- describe('FeatureFlagClient', () => {
17
- beforeEach(() => {
18
- mockFetch.mockReset();
19
- });
20
-
21
- test('init fetches flags and marks as initialized', async () => {
22
- mockFetchResponse({
23
- flags: {
24
- 'dark-mode': { value: true, type: 'boolean' },
25
- 'variant': { value: 'b', type: 'string' },
26
- },
27
- });
28
-
29
- const client = new FeatureFlagClient({
30
- secretApiKey: 'test-key',
31
- environment: 'production',
32
- propertyId: 'test-prop',
33
- apiUrl: 'http://localhost:3001',
34
- });
35
-
36
- expect(client.isInitialized()).toBe(false);
37
- await client.init({ userId: 'u1' });
38
- expect(client.isInitialized()).toBe(true);
39
-
40
- // Verify fetch was called correctly
41
- expect(mockFetch).toHaveBeenCalledTimes(1);
42
- const [url, opts] = mockFetch.mock.calls[0] as any[];
43
- expect(url).toBe('http://localhost:3001/v1/flags/evaluate');
44
- expect(opts.method).toBe('POST');
45
- expect(opts.headers['x-api-key']).toBe('test-key');
46
- const body = JSON.parse(opts.body);
47
- expect(body.environment).toBe('production');
48
- expect(body.context.userId).toBe('u1');
49
- });
50
-
51
- test('getBooleanFlag returns value when type matches', async () => {
52
- mockFetchResponse({
53
- flags: { 'dark-mode': { value: true, type: 'boolean' } },
54
- });
55
-
56
- const client = new FeatureFlagClient({ secretApiKey: 'k', environment: 'prod', propertyId: 'test-prop', apiUrl: 'http://localhost:3001' });
57
- await client.init();
58
-
59
- expect(client.getBooleanFlag('dark-mode', false)).toBe(true);
60
- });
61
-
62
- test('getBooleanFlag returns default when flag missing', async () => {
63
- mockFetchResponse({ flags: {} });
64
-
65
- const client = new FeatureFlagClient({ secretApiKey: 'k', environment: 'prod', propertyId: 'test-prop', apiUrl: 'http://localhost:3001' });
66
- await client.init();
67
-
68
- expect(client.getBooleanFlag('missing', false)).toBe(false);
69
- });
70
-
71
- test('getBooleanFlag returns default when type is string', async () => {
72
- mockFetchResponse({
73
- flags: { 'variant': { value: 'a', type: 'string' } },
74
- });
75
-
76
- const client = new FeatureFlagClient({ secretApiKey: 'k', environment: 'prod', propertyId: 'test-prop', apiUrl: 'http://localhost:3001' });
77
- await client.init();
78
-
79
- expect(client.getBooleanFlag('variant', false)).toBe(false);
80
- });
81
-
82
- test('getStringFlag returns value when type matches', async () => {
83
- mockFetchResponse({
84
- flags: { 'variant': { value: 'checkout-b', type: 'string' } },
85
- });
86
-
87
- const client = new FeatureFlagClient({ secretApiKey: 'k', environment: 'prod', propertyId: 'test-prop', apiUrl: 'http://localhost:3001' });
88
- await client.init();
89
-
90
- expect(client.getStringFlag('variant', 'control')).toBe('checkout-b');
91
- });
92
-
93
- test('getStringFlag returns default when flag missing', async () => {
94
- mockFetchResponse({ flags: {} });
95
-
96
- const client = new FeatureFlagClient({ secretApiKey: 'k', environment: 'prod', propertyId: 'test-prop', apiUrl: 'http://localhost:3001' });
97
- await client.init();
98
-
99
- expect(client.getStringFlag('missing', 'default')).toBe('default');
100
- });
101
-
102
- test('getStringFlag returns default when type is boolean', async () => {
103
- mockFetchResponse({
104
- flags: { 'toggle': { value: true, type: 'boolean' } },
105
- });
106
-
107
- const client = new FeatureFlagClient({ secretApiKey: 'k', environment: 'prod', propertyId: 'test-prop', apiUrl: 'http://localhost:3001' });
108
- await client.init();
109
-
110
- expect(client.getStringFlag('toggle', 'fallback')).toBe('fallback');
111
- });
112
-
113
- test('refresh re-fetches flags with updated context', async () => {
114
- mockFetchResponse({ flags: { 'f': { value: false, type: 'boolean' } } });
115
-
116
- const client = new FeatureFlagClient({ secretApiKey: 'k', environment: 'prod', propertyId: 'test-prop', apiUrl: 'http://localhost:3001' });
117
- await client.init({ userId: 'u1' });
118
-
119
- mockFetchResponse({ flags: { 'f': { value: true, type: 'boolean' } } });
120
- await client.refresh({ userId: 'u2' });
121
-
122
- expect(mockFetch).toHaveBeenCalledTimes(2);
123
- expect(client.getBooleanFlag('f', false)).toBe(true);
124
-
125
- // Verify second call has updated context
126
- const [, opts] = mockFetch.mock.calls[1] as any[];
127
- const body = JSON.parse(opts.body);
128
- expect(body.context.userId).toBe('u2');
129
- });
130
-
131
- test('destroy clears state', async () => {
132
- mockFetchResponse({ flags: { 'f': { value: true, type: 'boolean' } } });
133
-
134
- const client = new FeatureFlagClient({ secretApiKey: 'k', environment: 'prod', propertyId: 'test-prop', apiUrl: 'http://localhost:3001' });
135
- await client.init();
136
-
137
- expect(client.getBooleanFlag('f', false)).toBe(true);
138
- expect(client.isInitialized()).toBe(true);
139
-
140
- client.destroy();
141
-
142
- expect(client.isInitialized()).toBe(false);
143
- expect(client.getBooleanFlag('f', false)).toBe(false);
144
- });
145
-
146
- test('handles fetch failure gracefully', async () => {
147
- mockFetch.mockRejectedValue(new Error('network error'));
148
-
149
- const client = new FeatureFlagClient({ secretApiKey: 'k', environment: 'prod', propertyId: 'test-prop', apiUrl: 'http://localhost:3001' });
150
- await client.init(); // should not throw
151
-
152
- // Returns defaults since no flags were loaded
153
- expect(client.getBooleanFlag('anything', true)).toBe(true);
154
- });
155
-
156
- test('handles non-ok response gracefully', async () => {
157
- mockFetch.mockResolvedValue(new Response('', { status: 500 }));
158
-
159
- const client = new FeatureFlagClient({ secretApiKey: 'k', environment: 'prod', propertyId: 'test-prop', apiUrl: 'http://localhost:3001' });
160
- await client.init(); // should not throw
161
-
162
- expect(client.getStringFlag('anything', 'default')).toBe('default');
163
- });
164
-
165
- test('strips trailing slash from apiUrl', async () => {
166
- mockFetchResponse({ flags: {} });
167
-
168
- const client = new FeatureFlagClient({ secretApiKey: 'k', environment: 'prod', propertyId: 'test-prop', apiUrl: 'http://localhost:3001/' });
169
- await client.init();
170
-
171
- const [url] = mockFetch.mock.calls[0] as any[];
172
- expect(url).toBe('http://localhost:3001/v1/flags/evaluate');
173
- });
174
-
175
- test('getFlags returns flag definitions', async () => {
176
- const flagsData = {
177
- flags: [
178
- { id: 'f1', key: 'dark-mode', name: 'Dark Mode', type: 'boolean', defaultValue: false, createdAt: 1000 },
179
- { id: 'f2', key: 'cta-color', name: 'CTA Color', type: 'string', defaultValue: 'blue', createdAt: 2000 },
180
- ],
181
- };
182
- mockFetchResponse(flagsData);
183
-
184
- const client = new FeatureFlagClient({ secretApiKey: 'test-key', environment: 'prod', propertyId: 'prop-1', apiUrl: 'http://localhost:3001' });
185
- const result = await client.getFlags();
186
-
187
- expect(result.flags).toHaveLength(2);
188
- expect(result.flags[0].key).toBe('dark-mode');
189
- expect(result.flags[1].key).toBe('cta-color');
190
-
191
- const [url, opts] = mockFetch.mock.calls[0] as any[];
192
- expect(url).toBe('http://localhost:3001/v1/flags/list?propertyId=prop-1');
193
- expect(opts.method).toBe('GET');
194
- expect(opts.headers['x-api-key']).toBe('test-key');
195
- });
196
-
197
- test('getFlags returns empty array on failure', async () => {
198
- mockFetch.mockRejectedValue(new Error('network error'));
199
-
200
- const client = new FeatureFlagClient({ secretApiKey: 'k', environment: 'prod', propertyId: 'p1', apiUrl: 'http://localhost:3001' });
201
- const result = await client.getFlags();
202
-
203
- expect(result.flags).toEqual([]);
204
- });
205
-
206
- test('defaults apiUrl to production', async () => {
207
- mockFetchResponse({ flags: {} });
208
-
209
- const client = new FeatureFlagClient({ secretApiKey: 'k', environment: 'prod', propertyId: 'test-prop' });
210
- await client.init();
211
-
212
- const [url] = mockFetch.mock.calls[0] as any[];
213
- expect(url).toBe('https://api.sessionsight.com/v1/flags/evaluate');
214
- });
215
- });
package/tsconfig.json DELETED
@@ -1,13 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "./dist",
5
- "declaration": true,
6
- "noEmit": false,
7
- "rootDir": "./src",
8
- "lib": ["ES2022", "DOM"],
9
- "module": "ESNext",
10
- "moduleResolution": "bundler"
11
- },
12
- "include": ["src/**/*.ts"]
13
- }