@ops-ai/react-router-toggly 1.1.0 → 1.2.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/CHANGELOG.md +11 -0
- package/dist/client.cjs +72 -7
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +26 -0
- package/dist/client.d.ts +26 -0
- package/dist/client.js +72 -7
- package/dist/client.js.map +1 -1
- package/dist/server.cjs +212 -64
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +49 -5
- package/dist/server.d.ts +49 -5
- package/dist/server.js +212 -64
- package/dist/server.js.map +1 -1
- package/dist/telemetry/grpc.cjs +1 -1
- package/dist/telemetry/grpc.cjs.map +1 -1
- package/dist/telemetry/grpc.js +1 -1
- package/dist/telemetry/grpc.js.map +1 -1
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -65,6 +65,14 @@ interface TogglyConfig {
|
|
|
65
65
|
localGates?: LocalGate[];
|
|
66
66
|
/** Optional SDK error callback for reporting fetch/evaluation failures. */
|
|
67
67
|
onError?: (message: string, error?: unknown) => void;
|
|
68
|
+
/**
|
|
69
|
+
* Use `/evaluated-variants-signed` and expose {@link TogglyContextValue.getVariant} /
|
|
70
|
+
* `getVariantValue` (client) or the equivalent {@link TogglyServerClient} methods
|
|
71
|
+
* (server). Only applies in `remote` evaluationMode client-side; the server
|
|
72
|
+
* package forces `remote` when this is set (mirrors the Nuxt/Astro OPS-825
|
|
73
|
+
* override — server otherwise defaults to `definitions-signed` + local eval).
|
|
74
|
+
*/
|
|
75
|
+
enableVariants?: boolean;
|
|
68
76
|
/** Host-minted browser capability; takes precedence over client identity targeting. */
|
|
69
77
|
instanceId?: string;
|
|
70
78
|
/** Initial browser identity; hydrated server identity takes precedence. */
|
|
@@ -144,6 +152,13 @@ interface IdentityContext {
|
|
|
144
152
|
* Feature flags state (boolean or entity gate per flag)
|
|
145
153
|
*/
|
|
146
154
|
type FeatureFlags = EvaluatedDefinitions;
|
|
155
|
+
/**
|
|
156
|
+
* Assigned variant for a feature (aligned with @ops-ai/feature-flags-toggly).
|
|
157
|
+
*/
|
|
158
|
+
interface VariantResult {
|
|
159
|
+
name: string;
|
|
160
|
+
configurationValue?: unknown;
|
|
161
|
+
}
|
|
147
162
|
/**
|
|
148
163
|
* Feature evaluation result
|
|
149
164
|
*/
|
|
@@ -276,6 +291,17 @@ interface TogglyContextValue extends FrontendTelemetry {
|
|
|
276
291
|
evaluateGate: (featureKeys: string[], requirement?: 'all' | 'any', negate?: boolean, entity?: TogglyEntityContext | Record<string, unknown> | null, kind?: string) => boolean;
|
|
277
292
|
/** Register a domain-object mapper for entity-context evaluation */
|
|
278
293
|
registerContext: <T>(kind: string, mapper: (entity: T) => TogglyEntityContext) => void;
|
|
294
|
+
/**
|
|
295
|
+
* Current variant assignment for a feature (requires `enableVariants` in the
|
|
296
|
+
* provider config). Soft-null when variants are disabled, unassigned, or the
|
|
297
|
+
* effective flag is off — never throws.
|
|
298
|
+
*/
|
|
299
|
+
getVariant: (featureKey: string) => VariantResult | null;
|
|
300
|
+
/**
|
|
301
|
+
* Configuration payload for the assigned variant, if any.
|
|
302
|
+
* Optional `isT` type guard soft-fails to null on mismatch.
|
|
303
|
+
*/
|
|
304
|
+
getVariantValue: <T = unknown>(featureKey: string, isT?: (v: unknown) => v is T) => T | null;
|
|
279
305
|
/** Set user identity */
|
|
280
306
|
identify: (identity: string, context?: IdentityContext) => Promise<void>;
|
|
281
307
|
/** Clear user identity */
|
package/dist/client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { registerContext, normalizeEntityContext, serializeJsonForInlineScript, resolveEvaluatedDefinition, appendEvaluationContext } from '@ops-ai/toggly-hooks-types';
|
|
2
2
|
export { normalizeEntityContext, registerContext } from '@ops-ai/toggly-hooks-types';
|
|
3
3
|
import '@ops-ai/toggly-eval';
|
|
4
|
+
import { asVariantDefsRecord, unwrapDefsPayload } from '@ops-ai/toggly-signed-defs';
|
|
4
5
|
import { createContext, useRef, useMemo, useState, useEffect, useCallback, useContext, useLayoutEffect } from 'react';
|
|
5
6
|
import { createTelemetryReporter } from '@ops-ai/toggly-client-telemetry';
|
|
6
7
|
import { attachBrowserLifecycle } from '@ops-ai/toggly-client-telemetry/browser';
|
|
@@ -51,12 +52,18 @@ function mergeConfig(config) {
|
|
|
51
52
|
};
|
|
52
53
|
}
|
|
53
54
|
function buildDefinitionsUrl(config, context) {
|
|
54
|
-
const { baseUrl, appKey, environment, groups, claims, evaluationMode } = mergeConfig(config);
|
|
55
|
+
const { baseUrl, appKey, environment, groups, claims, evaluationMode, enableVariants } = mergeConfig(config);
|
|
55
56
|
if (!appKey) {
|
|
56
57
|
throw new Error("appKey is required");
|
|
57
58
|
}
|
|
58
59
|
const mode = evaluationMode ?? "remote";
|
|
59
|
-
const
|
|
60
|
+
const useVariants = mode !== "local" && enableVariants === true;
|
|
61
|
+
let pathSegment = "evaluated-signed";
|
|
62
|
+
if (mode === "local") {
|
|
63
|
+
pathSegment = "definitions-signed";
|
|
64
|
+
} else if (useVariants) {
|
|
65
|
+
pathSegment = "evaluated-variants-signed";
|
|
66
|
+
}
|
|
60
67
|
const url = new URL(`${baseUrl}/${pathSegment}/${appKey}/${environment}`);
|
|
61
68
|
if (mode === "local") {
|
|
62
69
|
return url.toString();
|
|
@@ -69,10 +76,25 @@ function buildDefinitionsUrl(config, context) {
|
|
|
69
76
|
groups: fromParam?.groups ?? groups,
|
|
70
77
|
claims: fromParam?.claims ?? claims
|
|
71
78
|
},
|
|
72
|
-
"evaluated"
|
|
79
|
+
useVariants ? "variants" : "evaluated"
|
|
73
80
|
);
|
|
74
81
|
return url.toString();
|
|
75
82
|
}
|
|
83
|
+
function parseVariantDefsPayload(parsed) {
|
|
84
|
+
return asVariantDefsRecord(unwrapDefsPayload(parsed));
|
|
85
|
+
}
|
|
86
|
+
function variantDefsToFlags(defs) {
|
|
87
|
+
const out = {};
|
|
88
|
+
for (const key of Object.keys(defs)) {
|
|
89
|
+
out[key] = defs[key]?.enabled === true;
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
function decodeVariantValue(value, isT) {
|
|
94
|
+
if (value === null || value === void 0) return null;
|
|
95
|
+
if (isT) return isT(value) ? value : null;
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
76
98
|
function isFeatureEnabled(flags, featureKey, defaultValue = false, entityContext) {
|
|
77
99
|
if (!flags || Object.keys(flags).length === 0) {
|
|
78
100
|
return defaultValue;
|
|
@@ -137,6 +159,12 @@ var HEADERS = {
|
|
|
137
159
|
CACHE_CONTROL: "cache-control"
|
|
138
160
|
};
|
|
139
161
|
var TOGGLY_LOADER_KEY = "__toggly";
|
|
162
|
+
function checkVariantLabel(result) {
|
|
163
|
+
if (typeof result === "string") {
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
return result ? "enabled" : "disabled";
|
|
167
|
+
}
|
|
140
168
|
function createBrowserTelemetry(config) {
|
|
141
169
|
const enabled = globalThis.window !== void 0 && typeof document !== "undefined" && !!config.appKey?.trim() && config.enableTelemetry !== false;
|
|
142
170
|
const usage = enabled && config.enableUsageTracking !== false;
|
|
@@ -265,13 +293,13 @@ function createBrowserTelemetry(config) {
|
|
|
265
293
|
};
|
|
266
294
|
if (active) {
|
|
267
295
|
const record = getReporter().captureCheck();
|
|
268
|
-
return (key, result) => record(key, result
|
|
296
|
+
return (key, result) => record(key, checkVariantLabel(result));
|
|
269
297
|
}
|
|
270
298
|
const attribution = context;
|
|
271
299
|
const capturedPartition = partition;
|
|
272
|
-
return (key, result) => feature(key, result
|
|
300
|
+
return (key, result) => feature(key, checkVariantLabel(result), 0, attribution, capturedPartition);
|
|
273
301
|
},
|
|
274
|
-
recordCheck: (key, result) => feature(key, result
|
|
302
|
+
recordCheck: (key, result) => feature(key, checkVariantLabel(result), 0),
|
|
275
303
|
activate() {
|
|
276
304
|
if (disposed || active) return;
|
|
277
305
|
active = true;
|
|
@@ -291,7 +319,7 @@ function createBrowserTelemetry(config) {
|
|
|
291
319
|
|
|
292
320
|
// src/client/sdk-identity.ts
|
|
293
321
|
var SDK_ID = "react-router";
|
|
294
|
-
var SDK_VERSION = "1.
|
|
322
|
+
var SDK_VERSION = "1.2.0";
|
|
295
323
|
function appendSdkQueryParams(params) {
|
|
296
324
|
params.set("sdk", SDK_ID);
|
|
297
325
|
params.set("sdkVersion", SDK_VERSION);
|
|
@@ -345,6 +373,7 @@ function TogglyProviderOwner({
|
|
|
345
373
|
claims: { ...mergedConfig?.claims }
|
|
346
374
|
});
|
|
347
375
|
const flagsRef = useRef(serverContext?.flags ?? mergedConfig?.featureDefaults ?? {});
|
|
376
|
+
const variantsRef = useRef(null);
|
|
348
377
|
const generationRef = useRef(0);
|
|
349
378
|
const updateRevisionRef = useRef(0);
|
|
350
379
|
const [telemetry] = useState(() => createBrowserTelemetry({ ...config, ...contextRef.current }));
|
|
@@ -482,6 +511,14 @@ function TogglyProviderOwner({
|
|
|
482
511
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
483
512
|
}
|
|
484
513
|
const payload = await response.json();
|
|
514
|
+
if (mergedConfig.enableVariants === true) {
|
|
515
|
+
const defs = parseVariantDefsPayload(payload);
|
|
516
|
+
variantsRef.current = defs;
|
|
517
|
+
const newFlags2 = variantDefsToFlags(defs);
|
|
518
|
+
logger.debug(`Fetched ${Object.keys(newFlags2).length} flags (variants).`);
|
|
519
|
+
return newFlags2;
|
|
520
|
+
}
|
|
521
|
+
variantsRef.current = null;
|
|
485
522
|
const newFlags = payload && typeof payload === "object" ? payload : {};
|
|
486
523
|
logger.debug(`Fetched ${Object.keys(newFlags).length} flags.`);
|
|
487
524
|
return newFlags;
|
|
@@ -541,10 +578,34 @@ function TogglyProviderOwner({
|
|
|
541
578
|
},
|
|
542
579
|
[captureEvaluation]
|
|
543
580
|
);
|
|
581
|
+
const getVariant = useCallback(
|
|
582
|
+
(featureKey) => {
|
|
583
|
+
if (!mergedConfig?.enableVariants) return null;
|
|
584
|
+
const entry = variantsRef.current?.[featureKey];
|
|
585
|
+
const enabled = applyLocalGate(
|
|
586
|
+
entry?.enabled === true,
|
|
587
|
+
featureKey,
|
|
588
|
+
localGatesRef.current,
|
|
589
|
+
localGateIndexRef.current
|
|
590
|
+
);
|
|
591
|
+
const record = telemetry.captureCheck();
|
|
592
|
+
record(featureKey, enabled && entry?.variant ? entry.variant : "disabled");
|
|
593
|
+
if (!enabled || !entry?.variant) return null;
|
|
594
|
+
return { name: entry.variant, configurationValue: entry.configurationValue };
|
|
595
|
+
},
|
|
596
|
+
[mergedConfig?.enableVariants, localGatesRevision, telemetry, flags]
|
|
597
|
+
);
|
|
598
|
+
const getVariantValue = useCallback(
|
|
599
|
+
(featureKey, isT) => {
|
|
600
|
+
return decodeVariantValue(getVariant(featureKey)?.configurationValue, isT);
|
|
601
|
+
},
|
|
602
|
+
[getVariant]
|
|
603
|
+
);
|
|
544
604
|
const installContext = useCallback((target) => {
|
|
545
605
|
contextRef.current = target;
|
|
546
606
|
telemetry.setContext(target);
|
|
547
607
|
flagsRef.current = mergedConfig?.featureDefaults ?? {};
|
|
608
|
+
variantsRef.current = null;
|
|
548
609
|
setFlags(flagsRef.current);
|
|
549
610
|
setIdentity(target.identity);
|
|
550
611
|
}, [mergedConfig?.featureDefaults, telemetry]);
|
|
@@ -815,6 +876,8 @@ function TogglyProviderOwner({
|
|
|
815
876
|
isDisabled,
|
|
816
877
|
evaluateGate,
|
|
817
878
|
registerContext: registerContext2,
|
|
879
|
+
getVariant,
|
|
880
|
+
getVariantValue,
|
|
818
881
|
identify,
|
|
819
882
|
reset,
|
|
820
883
|
refresh,
|
|
@@ -833,6 +896,8 @@ function TogglyProviderOwner({
|
|
|
833
896
|
isDisabled,
|
|
834
897
|
evaluateGate,
|
|
835
898
|
registerContext2,
|
|
899
|
+
getVariant,
|
|
900
|
+
getVariantValue,
|
|
836
901
|
identify,
|
|
837
902
|
reset,
|
|
838
903
|
refresh,
|