Skip to content

Commit d6e05a3

Browse files
committed
first stab
1 parent c4030e2 commit d6e05a3

File tree

10 files changed

+145
-29
lines changed

10 files changed

+145
-29
lines changed

packages/browser-sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
},
3838
"dependencies": {
3939
"@floating-ui/dom": "^1.6.8",
40-
"canonical-json": "^0.0.4",
40+
"canonical-json": "^0.2.0",
4141
"js-cookie": "^3.0.5",
4242
"preact": "^10.22.1"
4343
},

packages/browser-sdk/src/client.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ export class ReflagClient {
388388
public readonly logger: Logger;
389389

390390
private readonly hooks: HooksManager;
391+
private liveTargetingFlags: Set<string> = new Set();
391392

392393
/**
393394
* Create a new ReflagClient instance.
@@ -850,6 +851,30 @@ export class ReflagClient {
850851
});
851852
}
852853

854+
/**
855+
* Set the live targeting flags from React SDK.
856+
* @internal
857+
*/
858+
setLiveTargetingFlags(flags: Set<string>) {
859+
this.liveTargetingFlags = flags;
860+
this.hooks.trigger("liveTargetingUpdated", flags);
861+
}
862+
863+
/**
864+
* Get the current live targeting flags.
865+
* @internal
866+
*/
867+
getLiveTargetingFlags(): Set<string> {
868+
return this.liveTargetingFlags;
869+
}
870+
871+
/**
872+
* Check if a flag is currently being used in React components.
873+
*/
874+
isFlagLiveTargeting(flagKey: string): boolean {
875+
return this.liveTargetingFlags.has(flagKey);
876+
}
877+
853878
/**
854879
* Stop the SDK.
855880
* This will stop any automated feedback surveys.

packages/browser-sdk/src/hooksManager.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface HookArgs {
1212
user: UserContext;
1313
company: CompanyContext;
1414
track: TrackEvent;
15+
liveTargetingUpdated: Set<string>;
1516
}
1617

1718
export type TrackEvent = {
@@ -32,12 +33,14 @@ export class HooksManager {
3233
user: ((arg0: UserContext) => void)[];
3334
company: ((arg0: CompanyContext) => void)[];
3435
track: ((arg0: TrackEvent) => void)[];
36+
liveTargetingUpdated: ((arg0: Set<string>) => void)[];
3537
} = {
3638
check: [],
3739
flagsUpdated: [],
3840
user: [],
3941
company: [],
4042
track: [],
43+
liveTargetingUpdated: [],
4144
};
4245

4346
private _adjustEvent(event: keyof HookArgs) {

packages/browser-sdk/src/toolbar/Flags.css

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,6 @@
6161
}
6262

6363
.flag-name-cell {
64-
white-space: nowrap;
65-
overflow: hidden;
66-
text-overflow: ellipsis;
6764
width: auto;
6865
padding: 6px 6px 6px 0;
6966
display: flex;
@@ -77,6 +74,21 @@
7774
}
7875
}
7976

77+
.flag-name-content {
78+
display: flex;
79+
align-items: center;
80+
gap: 4px;
81+
}
82+
83+
.live-targeting-indicator {
84+
color: var(--brand400);
85+
line-height: 0;
86+
}
87+
.live-targeting-indicator svg {
88+
width: 14px;
89+
height: 14px;
90+
}
91+
8092
.flag-link {
8193
color: var(--text-color);
8294
text-decoration: none;

packages/browser-sdk/src/toolbar/Flags.tsx

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -100,15 +100,32 @@ function FlagRow({
100100
class={["flag-row", isNotVisible ? "not-visible" : undefined].join(" ")}
101101
>
102102
<td class="flag-name-cell">
103-
<a
104-
class="flag-link"
105-
href={`${appBaseUrl}/env-current/flags/by-key/${flag.flagKey}`}
106-
rel="noreferrer"
107-
tabIndex={index + 1}
108-
target="_blank"
109-
>
110-
{flag.flagKey}
111-
</a>
103+
<div class="flag-name-content">
104+
{flag.isLiveTargeting && (
105+
<span
106+
class="live-targeting-indicator"
107+
data-tooltip="Currently mounted in React"
108+
data-tooltip-left
109+
>
110+
<svg
111+
xmlns="http://www.w3.org/2000/svg"
112+
viewBox="0 0 24 24"
113+
fill="currentColor"
114+
>
115+
<path d="M13 19.9381C16.6187 19.4869 19.4869 16.6187 19.9381 13H17V11H19.9381C19.4869 7.38128 16.6187 4.51314 13 4.06189V7H11V4.06189C7.38128 4.51314 4.51314 7.38128 4.06189 11H7V13H4.06189C4.51314 16.6187 7.38128 19.4869 11 19.9381V17H13V19.9381ZM12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 14C10.8954 14 10 13.1046 10 12C10 10.8954 10.8954 10 12 10C13.1046 10 14 10.8954 14 12C14 13.1046 13.1046 14 12 14Z" />
116+
</svg>
117+
</span>
118+
)}
119+
<a
120+
class="flag-link"
121+
href={`${appBaseUrl}/env-current/flags/by-key/${flag.flagKey}`}
122+
rel="noreferrer"
123+
tabIndex={index + 1}
124+
target="_blank"
125+
>
126+
{flag.flagKey}
127+
</a>
128+
</div>
112129
</td>
113130
<td class="flag-reset-cell">
114131
{flag.localOverride !== null ? (

packages/browser-sdk/src/toolbar/Toolbar.css

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -246,12 +246,13 @@
246246
content: attr(data-tooltip);
247247
position: absolute;
248248
right: 100%;
249-
top: 0%;
249+
top: 50%;
250+
transform: translateY(-50%);
250251
margin-right: 3px;
251252
user-select: none;
252253
pointer-events: none;
253-
background-color: var(--bg-color);
254-
border: 1px solid var(--border-color);
254+
background-color: var(--gray800);
255+
border: 1px solid var(--gray600);
255256
border-radius: 4px;
256257
padding: 0px 6px;
257258
height: 28px;
@@ -260,8 +261,15 @@
260261
font-size: var(--text-small-size);
261262
font-weight: normal;
262263
width: max-content;
263-
display: none;
264264
box-sizing: border-box;
265+
display: none;
266+
}
267+
268+
[data-tooltip][data-tooltip-left]:after {
269+
right: unset;
270+
margin-right: unset;
271+
left: 100%;
272+
margin-left: 3px;
265273
}
266274

267275
[data-tooltip]:hover:after {

packages/browser-sdk/src/toolbar/Toolbar.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@ export type FlagItem = {
2323
flagKey: string;
2424
localOverride: boolean | null;
2525
isEnabled: boolean;
26+
isLiveTargeting: boolean;
2627
};
2728

2829
type Flag = {
2930
flagKey: string;
3031
isEnabled: boolean;
3132
localOverride: boolean | null;
33+
isLiveTargeting: boolean;
3234
};
3335

3436
export default function Toolbar({
@@ -57,6 +59,7 @@ export default function Toolbar({
5759
flagKey: flag.key,
5860
localOverride: flag.isEnabledOverride,
5961
isEnabled: flag.isEnabled,
62+
isLiveTargeting: reflagClient.isFlagLiveTargeting(flag.key),
6063
}) satisfies FlagItem,
6164
),
6265
);
@@ -69,6 +72,7 @@ export default function Toolbar({
6972
useEffect(() => {
7073
updateFlags();
7174
reflagClient.on("flagsUpdated", updateFlags);
75+
reflagClient.on("liveTargetingUpdated", updateFlags);
7276
}, [reflagClient, updateFlags]);
7377

7478
const [search, setSearch] = useState<string | null>(null);

packages/react-sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
},
3636
"dependencies": {
3737
"@reflag/browser-sdk": "1.1.0",
38-
"canonical-json": "^0.0.4",
38+
"json-canonicalize": "^2.0.0",
3939
"rollup": "^4.2.0"
4040
},
4141
"peerDependencies": {

packages/react-sdk/src/index.tsx

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
import React, {
44
createContext,
55
ReactNode,
6+
useCallback,
67
useContext,
78
useEffect,
89
useRef,
910
useState,
1011
} from "react";
11-
import canonicalJSON from "canonical-json";
12+
import { canonicalize } from "json-canonicalize";
1213

1314
import {
1415
CheckEvent,
@@ -122,6 +123,9 @@ type ProviderContextType = {
122123
isLoading: boolean;
123124
};
124125
provider: boolean;
126+
liveTargetingFlags: Set<string>;
127+
registerLiveTargetingFlag: (flagKey: string) => void;
128+
unregisterLiveTargetingFlag: (flagKey: string) => void;
125129
};
126130

127131
const ProviderContext = createContext<ProviderContextType>({
@@ -130,6 +134,13 @@ const ProviderContext = createContext<ProviderContextType>({
130134
isLoading: false,
131135
},
132136
provider: false,
137+
liveTargetingFlags: new Set(),
138+
registerLiveTargetingFlag: () => {
139+
// No-op default implementation
140+
},
141+
unregisterLiveTargetingFlag: () => {
142+
// No-op default implementation
143+
},
133144
});
134145

135146
/**
@@ -176,12 +187,15 @@ export function ReflagProvider({
176187
}: ReflagProps) {
177188
const [featuresLoading, setFlagsLoading] = useState(true);
178189
const [rawFlags, setRawFlags] = useState<RawFlags>({});
190+
const [liveTargetingFlags, setLiveTargetingFlags] = useState<Set<string>>(
191+
new Set(),
192+
);
179193

180194
const clientRef = useRef<ReflagClient>();
181195
const contextKeyRef = useRef<string>();
182196

183197
const featureContext = { user, company, otherContext };
184-
const contextKey = canonicalJSON({ config, featureContext });
198+
const contextKey = canonicalize({ config, featureContext });
185199

186200
useEffect(() => {
187201
// useEffect will run twice in development mode
@@ -223,13 +237,35 @@ export function ReflagProvider({
223237
// eslint-disable-next-line react-hooks/exhaustive-deps -- should only run once
224238
}, [contextKey]);
225239

240+
const registerLiveTargetingFlag = useCallback((flagKey: string) => {
241+
setLiveTargetingFlags((prev) => {
242+
const newSet = new Set(prev).add(flagKey);
243+
// Sync with browser SDK client
244+
clientRef.current?.setLiveTargetingFlags(newSet);
245+
return newSet;
246+
});
247+
}, []);
248+
249+
const unregisterLiveTargetingFlag = useCallback((flagKey: string) => {
250+
setLiveTargetingFlags((prev) => {
251+
const newSet = new Set(prev);
252+
newSet.delete(flagKey);
253+
// Sync with browser SDK client
254+
clientRef.current?.setLiveTargetingFlags(newSet);
255+
return newSet;
256+
});
257+
}, []);
258+
226259
const context: ProviderContextType = {
227260
features: {
228261
features: rawFlags,
229262
isLoading: featuresLoading,
230263
},
231264
client: clientRef.current,
232265
provider: true,
266+
liveTargetingFlags,
267+
registerLiveTargetingFlag,
268+
unregisterLiveTargetingFlag,
233269
};
234270
return (
235271
<ProviderContext.Provider value={context}>
@@ -267,8 +303,19 @@ export function useFlag<TKey extends FlagKey>(key: TKey): TypedFlags[TKey] {
267303
const client = useClient();
268304
const {
269305
features: { isLoading },
306+
registerLiveTargetingFlag,
307+
unregisterLiveTargetingFlag,
270308
} = useContext<ProviderContextType>(ProviderContext);
271309

310+
// Track mount/unmount for live targeting indicator
311+
useEffect(() => {
312+
console.log("registering live targeting flag", key);
313+
registerLiveTargetingFlag(key);
314+
return () => {
315+
unregisterLiveTargetingFlag(key);
316+
};
317+
}, [key, registerLiveTargetingFlag, unregisterLiveTargetingFlag]);
318+
272319
const track = () => client?.track(key);
273320
const requestFeedback = (opts: RequestFeedbackOptions) =>
274321
client?.requestFeedback({ ...opts, flagKey: key });

yarn.lock

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2964,7 +2964,7 @@ __metadata:
29642964
"@types/node": "npm:^22.12.0"
29652965
"@vitest/coverage-v8": "npm:^2.0.4"
29662966
c8: "npm:~10.1.3"
2967-
canonical-json: "npm:^0.0.4"
2967+
canonical-json: "npm:^0.2.0"
29682968
eslint: "npm:^9.21.0"
29692969
eslint-config-preact: "npm:^1.5.0"
29702970
http-server: "npm:^14.1.1"
@@ -3136,9 +3136,9 @@ __metadata:
31363136
"@types/react": "npm:^18.3.2"
31373137
"@types/react-dom": "npm:^18.3.0"
31383138
"@types/webpack": "npm:^5.28.5"
3139-
canonical-json: "npm:^0.0.4"
31403139
eslint: "npm:^9.21.0"
31413140
jsdom: "npm:^24.1.0"
3141+
json-canonicalize: "npm:^2.0.0"
31423142
msw: "npm:^2.3.5"
31433143
prettier: "npm:^3.5.2"
31443144
react: "npm:*"
@@ -6597,13 +6597,6 @@ __metadata:
65976597
languageName: node
65986598
linkType: hard
65996599

6600-
"canonical-json@npm:^0.0.4":
6601-
version: 0.0.4
6602-
resolution: "canonical-json@npm:0.0.4"
6603-
checksum: 10c0/bf130f4f6284a84860e16ef43a005544c98b8d334c128d3b3af006a586b4666b228bc2c5ef20ff9bedeaeffa45b2e862ad5e26eb6635dae7237c7c8b50f73096
6604-
languageName: node
6605-
linkType: hard
6606-
66076600
"canonical-json@npm:^0.2.0":
66086601
version: 0.2.0
66096602
resolution: "canonical-json@npm:0.2.0"
@@ -11874,6 +11867,13 @@ __metadata:
1187411867
languageName: node
1187511868
linkType: hard
1187611869

11870+
"json-canonicalize@npm:^2.0.0":
11871+
version: 2.0.0
11872+
resolution: "json-canonicalize@npm:2.0.0"
11873+
checksum: 10c0/35c698a4b8aa6afb274b2144e778eaae6a58d9b18f1738a5118b70daa92542b2eed7ec75b2eea39d6e448e57527f267d0c47f12eae7e205e350a546d9f5d5f6b
11874+
languageName: node
11875+
linkType: hard
11876+
1187711877
"json-parse-better-errors@npm:^1.0.1":
1187811878
version: 1.0.2
1187911879
resolution: "json-parse-better-errors@npm:1.0.2"

0 commit comments

Comments
 (0)