@ops-ai/react-router-toggly 1.2.1 → 1.5.2

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 CHANGED
@@ -1,5 +1,59 @@
1
+ ## 1.5.2
2
+
3
+ 2026-11-08
4
+
5
+ ### Fixed
6
+ - The browser client unwraps `evaluated-signed` `{ defs }` envelopes before
7
+ storing flags. A bare flag map still works. Previously the signed envelope
8
+ was stored as the flag map, so flag keys never matched [OPS-2064].
9
+
10
+ ### Changed
11
+ - `engines.node` floor raised from `>=18` to `>=22` (Node 18 and 20 are EOL).
12
+ - CI unit matrix is Node 22 / 24 / 26; packed RR7 and RR8 hosts run on Node 24.
13
+ - Packed hosts pin React Router 7.18.5 + React 18 and React Router 8.5.1 + React 19.3.1.
14
+ - Non-breaking dependency bumps within existing major ranges (`typescript` 5.9,
15
+ `ts-jest`, `@grpc/*`, `@ops-ai/toggly-*` patches/minors, `ws`).
16
+
17
+ ### Tests
18
+ - Added mocked HTTP API-contract evaluation coverage for `definitions-signed`
19
+ (local server eval) and `evaluated-signed` (browser client) response shapes,
20
+ including a `{ defs }` envelope and a bare flag map.
21
+
22
+ ## 1.5.1
23
+
24
+ 2026-10-15
25
+
26
+ ### Fixed
27
+ - After `dispose()`, `use()` and `captureCheck()` no longer create an
28
+ unmanaged reporter or attach leftover lifecycle listeners [OPS-1758].
29
+
30
+ ## 1.5.0
31
+
32
+ 2026-10-11
33
+
34
+ ### Added
35
+ - `use()` for timed feature usage scopes (attempt, duration, errors) via `@ops-ai/toggly-client-telemetry` [OPS-1758].
36
+
1
37
  # Changelog
2
38
 
39
+ ## [1.4.0] - 2026-10-11
40
+
41
+ ### Added
42
+ - Usage scope metrics on `VariantStats` (`errorCount`, `durationSumMs`,
43
+ `errorDurationSumMs`) and batcher APIs `recordUsageError`, `recordUsageDuration`,
44
+ `recordUsageScopeOutcome` (proto fields 5–7).
45
+
46
+ ## [1.3.1] - 2026-10-11
47
+
48
+ ### Fixed
49
+ - Only call `useRouteLoaderData` when `routeId` is set and use `useInRouterContext` so SSR and non-data-router apps hydrate from `fallbackContext` / `TogglyScript` without invoking data-router hooks [OPS-1756].
50
+
51
+ ## [1.3.0] - 2026-10-04
52
+
53
+ ### Added
54
+ - Server export of `createSegmentMembershipClient` for Backend-key targeting-list updates.
55
+
56
+
3
57
  ## [1.2.1] - 2026-09-27
4
58
 
5
59
  ### Fixed
package/README.md CHANGED
@@ -64,7 +64,7 @@ npm install @ops-ai/react-router-toggly react-router react react-dom
64
64
  ```
65
65
 
66
66
  Peers: `react-router ^7 || ^8`, `react` / `react-dom ^18 || ^19`. Optional:
67
- `@react-router/node`. Requires Node `>=18`.
67
+ `@react-router/node`. Requires Node `>=22`.
68
68
 
69
69
  ## Quick start
70
70
 
package/dist/client.cjs CHANGED
@@ -84,6 +84,13 @@ function buildDefinitionsUrl(config, context) {
84
84
  function parseVariantDefsPayload(parsed) {
85
85
  return togglySignedDefs.asVariantDefsRecord(togglySignedDefs.unwrapDefsPayload(parsed));
86
86
  }
87
+ function parseEvaluatedFlagsPayload(parsed) {
88
+ const body = togglySignedDefs.unwrapDefsPayload(parsed);
89
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
90
+ return {};
91
+ }
92
+ return body;
93
+ }
87
94
  function variantDefsToFlags(defs) {
88
95
  const out = {};
89
96
  for (const key of Object.keys(defs)) {
@@ -186,6 +193,7 @@ function createBrowserTelemetry(config) {
186
193
  }
187
194
  };
188
195
  const getReporter = () => {
196
+ if (disposed) return void 0;
189
197
  if (!reporter) {
190
198
  reporter = togglyClientTelemetry.createTelemetryReporter({
191
199
  appKey: config.appKey,
@@ -207,6 +215,7 @@ function createBrowserTelemetry(config) {
207
215
  };
208
216
  function replay(entry) {
209
217
  const target = getReporter();
218
+ if (!target) return;
210
219
  target.setContext(entry.context);
211
220
  if (entry.kind === "feature") {
212
221
  const methods = ["recordCheck", "recordUsage", "recordView"];
@@ -274,6 +283,28 @@ function createBrowserTelemetry(config) {
274
283
  }
275
284
  const api = {
276
285
  recordUsage: (key, variant = "enabled") => feature(key, variant, 1),
286
+ use: (key, fn, variant = "enabled") => {
287
+ if (!usage || disposed) {
288
+ return togglyClientTelemetry.runFeatureUsageScope(
289
+ { recordAttempt: () => {
290
+ }, recordSuccessDuration: () => {
291
+ }, recordFailure: () => {
292
+ } },
293
+ fn
294
+ );
295
+ }
296
+ const target = getReporter();
297
+ if (!target) {
298
+ return togglyClientTelemetry.runFeatureUsageScope(
299
+ { recordAttempt: () => {
300
+ }, recordSuccessDuration: () => {
301
+ }, recordFailure: () => {
302
+ } },
303
+ fn
304
+ );
305
+ }
306
+ return target.use(key, fn, variant);
307
+ },
277
308
  recordView: (key, variant = "enabled") => feature(key, variant, 2),
278
309
  incrementCounter: (key, value = 1) => event({ partition, context, key, kind: "counter", values: [value] }),
279
310
  setGauge: (key, value) => event({ partition, context, key, kind: "gauge", values: [value] }),
@@ -293,7 +324,10 @@ function createBrowserTelemetry(config) {
293
324
  if (disposed || !usage) return () => {
294
325
  };
295
326
  if (active) {
296
- const record = getReporter().captureCheck();
327
+ const target = getReporter();
328
+ if (!target) return () => {
329
+ };
330
+ const record = target.captureCheck();
297
331
  return (key, result) => record(key, checkVariantLabel(result));
298
332
  }
299
333
  const attribution = context;
@@ -320,7 +354,7 @@ function createBrowserTelemetry(config) {
320
354
 
321
355
  // src/client/sdk-identity.ts
322
356
  var SDK_ID = "react-router";
323
- var SDK_VERSION = "1.2.1";
357
+ var SDK_VERSION = "1.5.0";
324
358
  function appendSdkQueryParams(params) {
325
359
  params.set("sdk", SDK_ID);
326
360
  params.set("sdkVersion", SDK_VERSION);
@@ -520,7 +554,7 @@ function TogglyProviderOwner({
520
554
  return newFlags2;
521
555
  }
522
556
  variantsRef.current = null;
523
- const newFlags = payload && typeof payload === "object" ? payload : {};
557
+ const newFlags = parseEvaluatedFlagsPayload(payload);
524
558
  logger.debug(`Fetched ${Object.keys(newFlags).length} flags.`);
525
559
  return newFlags;
526
560
  } catch (error) {
@@ -1053,32 +1087,97 @@ function FeatureGate({
1053
1087
  const enabled = useFeatureGate(featureKeys, requirement, negate, context, contextKind);
1054
1088
  return enabled ? /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children }) : null;
1055
1089
  }
1056
- function RouterTogglyProvider({
1090
+ function resolveServerContext(data, fallbackContext) {
1091
+ const serverContext = data?.[TOGGLY_LOADER_KEY];
1092
+ return serverContext ?? fallbackContext;
1093
+ }
1094
+ function buildMergedConfig(config, serverContext) {
1095
+ return config ?? (serverContext ? {
1096
+ appKey: serverContext.appKey,
1097
+ environment: serverContext.environment
1098
+ } : void 0);
1099
+ }
1100
+ function RouterTogglyProviderInner({
1101
+ children,
1102
+ serverContext,
1103
+ config,
1104
+ ...props
1105
+ }) {
1106
+ const mergedConfig = buildMergedConfig(config, serverContext);
1107
+ return /* @__PURE__ */ jsxRuntime.jsx(TogglyProvider, { serverContext, config: mergedConfig, ...props, children });
1108
+ }
1109
+ function RouterTogglyProviderWithData({
1110
+ children,
1111
+ data,
1112
+ fallbackContext,
1113
+ config,
1114
+ ...props
1115
+ }) {
1116
+ const serverContext = resolveServerContext(data, fallbackContext);
1117
+ return /* @__PURE__ */ jsxRuntime.jsx(RouterTogglyProviderInner, { serverContext, config, ...props, children });
1118
+ }
1119
+ function RouterTogglyProviderFromRoute({
1057
1120
  children,
1058
1121
  routeId,
1059
1122
  fallbackContext,
1060
1123
  config,
1061
1124
  ...props
1062
1125
  }) {
1063
- const routeData = reactRouter.useRouteLoaderData(routeId ?? "");
1126
+ const routeData = reactRouter.useRouteLoaderData(routeId);
1127
+ return /* @__PURE__ */ jsxRuntime.jsx(
1128
+ RouterTogglyProviderWithData,
1129
+ {
1130
+ data: routeData,
1131
+ fallbackContext,
1132
+ config,
1133
+ ...props,
1134
+ children
1135
+ }
1136
+ );
1137
+ }
1138
+ function RouterTogglyProviderFromLoader({
1139
+ children,
1140
+ fallbackContext,
1141
+ config,
1142
+ ...props
1143
+ }) {
1064
1144
  const loaderData = reactRouter.useLoaderData();
1065
- const data = routeId ? routeData : loaderData;
1066
- const serverContext = data?.[TOGGLY_LOADER_KEY];
1067
- const resolvedServerContext = serverContext ?? fallbackContext;
1068
- const mergedConfig = config ?? (resolvedServerContext ? {
1069
- appKey: resolvedServerContext.appKey,
1070
- environment: resolvedServerContext.environment
1071
- } : void 0);
1072
1145
  return /* @__PURE__ */ jsxRuntime.jsx(
1073
- TogglyProvider,
1146
+ RouterTogglyProviderWithData,
1074
1147
  {
1075
- serverContext: resolvedServerContext,
1076
- config: mergedConfig,
1148
+ data: loaderData,
1149
+ fallbackContext,
1150
+ config,
1077
1151
  ...props,
1078
1152
  children
1079
1153
  }
1080
1154
  );
1081
1155
  }
1156
+ function RouterTogglyProviderStatic({
1157
+ children,
1158
+ fallbackContext,
1159
+ config,
1160
+ ...props
1161
+ }) {
1162
+ const serverContext = fallbackContext ?? getWindowTogglyData();
1163
+ return /* @__PURE__ */ jsxRuntime.jsx(RouterTogglyProviderInner, { serverContext, config, ...props, children });
1164
+ }
1165
+ function RouterTogglyProvider({
1166
+ children,
1167
+ routeId,
1168
+ fallbackContext,
1169
+ config,
1170
+ ...props
1171
+ }) {
1172
+ const inRouter = reactRouter.useInRouterContext();
1173
+ if (routeId && inRouter) {
1174
+ return /* @__PURE__ */ jsxRuntime.jsx(RouterTogglyProviderFromRoute, { routeId, fallbackContext, config, ...props, children });
1175
+ }
1176
+ if (inRouter) {
1177
+ return /* @__PURE__ */ jsxRuntime.jsx(RouterTogglyProviderFromLoader, { fallbackContext, config, ...props, children });
1178
+ }
1179
+ return /* @__PURE__ */ jsxRuntime.jsx(RouterTogglyProviderStatic, { fallbackContext, config, ...props, children });
1180
+ }
1082
1181
  function useTogglyLoaderData() {
1083
1182
  const loaderData = reactRouter.useLoaderData();
1084
1183
  return {