Skip to content

deps: update dependency flagsmith/sdk-spec to v0.7.0 - #93

Open
renovate[bot] wants to merge 2 commits into
mainfrom
renovate/sdk-spec
Open

renovate[bot] wants to merge 2 commits into
mainfrom
renovate/sdk-spec

Conversation

@renovate

@renovate renovate Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

This PR contains the following updates:

Package Update Change
Flagsmith/sdk-spec minor v0.5.0 → v0.7.0

Release Notes

Flagsmith/sdk-spec

v0.7.0

Compare Source

What's Changed
New Contributors

Full Changelog: Flagsmith/sdk-spec@v0.6.0...v0.7.0

v0.6.0

Compare Source

What's Changed
New Contributors

Full Changelog: Flagsmith/sdk-spec@v0.5.1...v0.6.0

v0.5.1

Compare Source

What's Changed
  • ci: Run the JS client and iOS against main in the compatibility matrix by @​khvn26 in #​12
  • feat: Optionally treat steps an SDK does not implement as expected failures by @​khvn26 in #​11
  • feat: Add a testChange SDK step by @​khvn26 in #​14

Full Changelog: Flagsmith/sdk-spec@v0.5.0...v0.5.1


Configuration

📅 Schedule: (UTC)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@renovate renovate Bot added the sdk-spec Bumps of the Flagsmith SDK behavioural spec label Oct 29, 2026
@renovate
renovate Bot requested a review from a team as a code owner October 29, 2026 14:35
@renovate
renovate Bot requested review from talissoncosta and removed request for a team October 29, 2026 14:35
@coderabbitai

coderabbitai Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 10246855-f2cd-481f-a86a-ef3ccfa4963d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
Walkthrough

The client adds configurable event tracking, including buffered event submission and a single retry for failed batches. It adds APIs to retrieve experiment flags and record eligible exposures. Event and flag models now represent event payloads, variants, and experiment metadata. Tests and SDK spec steps cover event tracking and experiment exposures. The SDK spec workflow reference and input both change to version v0.6.0.

Priority: Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: Low · previous review a6265

Experiment exposures can send traits marked transient to the events API. Filter those traits before delivery; the remaining risk is limited to this exposure path.

- [ ] Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@flagsmithsmith flagsmithsmith Bot changed the title Update dependency Flagsmith/sdk-spec to v0.6.0 deps: update dependency flagsmith/sdk-spec to v0.6.0 Oct 29, 2026
@renovate renovate Bot changed the title deps: update dependency flagsmith/sdk-spec to v0.6.0 Update dependency Flagsmith/sdk-spec to v0.6.0 Oct 29, 2026
@renovate renovate Bot changed the title Update dependency Flagsmith/sdk-spec to v0.6.0 deps: update dependency flagsmith/sdk-spec to v0.6.0 Oct 29, 2026
@flagsmithsmith flagsmithsmith Bot changed the title deps: update dependency flagsmith/sdk-spec to v0.6.0 feat: Track events and exposures, and get experiment flags (SDK spec v0.6.0) Oct 29, 2026
@flagsmithsmith

Copy link
Copy Markdown
Contributor

SDK spec v0.6.0 adds @events scenarios: getting an experiment flag, tracking an event, and tracking an exposure. This SDK had no way to send events, so the spec failed with "The SDK does not implement the step" for 5 of its 7 scenarios. This change implements the feature in the SDK and wires the new steps to it.

Changes

  • New opt-in events pipeline: Flagsmith(enableEvents = true, eventsBaseUrl = …, eventsFlushPeriod = …). Events are buffered and sent in batches to {eventsBaseUrl}v1/events with the environment key. Repeated exposures in a batch are sent once, and a failed batch is retried once. This matches the JS and Flutter SDKs.
  • trackEvent(event, identity, value, traits, metadata). Names starting with $ are reserved.
  • trackExposureEvent(featureName, identity, value, traits, metadata), sent as $flag_exposure.
  • getExperimentFlag(featureName, identity, traits) resolves the flag for the identity. When the identity is enrolled in a running experiment, it records an exposure to the served variant, with the identity's traits and experiment_id.
  • Flag now exposes variant and metadata.experiment from the API response.
  • sdk-spec/steps.kt wires the new steps to these calls. The JVM android.util.Log stub gains w.
  • Unit tests in EventsTests cover each behaviour.

I ran the v0.6.0 spec locally against its compose stack (7/7 passed) and ran ./gradlew check (58 tests passed). I also retitled the PR as a Conventional Commit, which the "PR Title" check requires.

Review effort: 3/5

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1


Review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: fc3cbae5-2654-4bd7-81f6-bbeef0799985
Commits

Reviewing files that changed from the base of the PR and between 169807e and a6265bd.

Files selected for processing: 11
  • .github/workflows/sdk-spec.yml
  • FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt
  • FlagsmithClient/src/main/java/com/flagsmith/entities/Event.kt
  • FlagsmithClient/src/main/java/com/flagsmith/entities/Flag.kt
  • FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithEventProcessor.kt
  • FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt
  • FlagsmithClient/src/test/java/com/flagsmith/EventsTests.kt
  • FlagsmithClient/src/test/java/com/flagsmith/SynchronousFlagsmith.kt
  • FlagsmithClient/src/test/java/com/flagsmith/mockResponses/MockResponses.kt
  • sdk-spec/src/main/java/android/util/Log.java
  • sdk-spec/steps.kt

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

featureName,
identity = identity,
value = flag.variant,
traits = traits?.associate { it.key to it.traitValue },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Minor · Security & Privacy · Quick win

Supported by static analysis

Web query:

Flagsmith SDK experimentation events transient traits not sent to v1/events

Result:

<source_evidence>
<source kind="open_page">
<title>flagsmith-js-client/flagsmith-core.ts at main · Flagsmith/flagsmith-js-client · GitHub</title>
<location>https://github.com/Flagsmith/flagsmith-js-client/blob/main/flagsmith-core.ts</location>
<excerpt>citeturn1view0 [wordlim: 200] Crawled: 3 weeks ago; Content type: text/html; Source: open({&quot;ref_id&quot;:&quot;turn0search11&quot;,&quot;lineno&quot;:null}); Total lines: 3960
L2226: import setDynatraceValue from &#39;./utils/set-dynatrace-value&#39;;
L2227: 
L2228: import { EvaluationContext } from &#39;./evaluation-context&#39;;
L2229: 
L2230: import { isTraitEvaluationContext, resolveTraitValues, toEvaluationContext, toTraitEvaluationContextObject } from &#39;./utils/types&#39;;
L2231: 
L2232: import { ensureTrailingSlash } from &#39;./utils/ensureTrailingSlash&#39;;
L2233: 
L2234: import { SDK_VERSION } from &#39;./utils/version&#39;;
L2235: 
L2236: import { EventProcessor, FLAG_EXPOSURE_EVENT } from &#39;./event-processor&#39;;
L2237: 
L2238: export enum FlagSource {
L2239: 
L2240: &quot;NONE&quot; = &quot;NONE&quot;,
L2312: constructor(props: Config) {
L2313: 
L2314: if (props.fetch) {
L2315: 
L2316: _fetch = props.fetch as LikeFetch;
L2317: 
L2318: } else {
L2319: 
L2320: _fetch = (typeof fetch !== &#39;undefined&#39; ? fetch : global?.fetch) as LikeFetch;
L2321: 
L2322: }
L2323: 
L2324: this.canUseStorage = typeof window !== &#39;undefined&#39; || !!props.browserlessStorage;
L2325: 
L2326: this.applicationMetadata = props.applicationMetadata;
L2327: 
L2328: this.log(&quot;Constructing flagsmith instance &quot; + props)
L2329: 
L2330: if (props.eventSource) {
L2331: 
L2332: eventSource = props.eventSource;
L2333: 
L2334: }
L2335: 
L2336: if (props.AsyncStorage) {
L2337: 
L2338: AsyncStorage = props.AsyncStorage;
L2339: 
L2340: }
L2341: 
L2342: }
L2343: getFlags = () =&gt; {
L2344: 
L2345: const { api, evaluationContext } = this;
L2346: 
L2347: this.log(&quot;Get Flags&quot;)
L2348: 
L2349: this.isLoading = true;
L2350: 
L2351: if (!this.loadingState.isFetching) {
L2352: 
L2353: this.setLoadingState({
L2354: 
L2355: ...this.loadingState,
L2356: 
L2357: isFetching: true
L2358: 
L2359: })
L2360: 
L2361: }
L2362: 
L2363: const previousIdentity = `${this.getContext().identity}`;
L2364: 
L2365: const handleResponse = (response: IFlagsmithResponse | null) =&gt; {
L2366: 
L2367: if(!response || previousIdentity !== `${this.getContext().identity}`) {
L2368: 
L2369: return // getJSON returned null due to request/response mismatch
L2370: 
L2371: }
L2372: let { flags: features, traits }: IFlagsmithResponse = response
L2373: 
L2374: const {identifier} = response
L2375: 
L2376: this.isLoading = false;
L2377: 
L2378: // Handle server response
L2379: 
L2380: const flags: IFlags = {};
L2381: 
L2382: const userTraits: Traits = {};
L2383: 
L2384: features = features || [];
L2385: 
L2386: traits = traits || [];
L2387: 
L2388: features.forEach(feature =&gt; {
L2389: 
L2390: const experiment = feature.metadata?.experiment;
L2391: 
L2392: flags[feature.feature.name.toLowerCase().replace(/ /g, &#39;_&#39;)] = {
L2393: 
L2394: id: feature.feature.id,
L2395: 
L2396: enabled: feature.enabled,
L2397: 
L2398: value: feature.feature_state_value,
L2399: ...(feature.variant ? { variant: feature.variant } : {}),
L2400: 
L2401: ...(feature.reason ? { reason: feature.reason } : {}),
L2402: 
L2403: ...(experiment ? {
L2404: 
L2405: experiment: {
L2406: 
L2407: id: experiment.id,
L2408: 
L2409: name: experiment.name,
L2410: 
L2411: inExperiment: !!experiment.in_experiment,
L2412: 
L2413: },
L2414: 
L2415: } : {}),
L2416: 
L2417: };
L2418: 
L2419: });
L2420: 
L2421: traits.forEach(trait =&gt; {
L2422: 
L2423: userTraits[trait.trait_key.replace(/ /g, &#39;_&#39;)] = {
L2424: 
L2425: transient: trait.transient,
L2426: 
L2427: value: trait.trait_value,
L2428: 
L2429: }
L2430: 
L2431: });
L2432: 
L2433: this.oldFlags = { ...this.flags };
L2434: 
L2435: const flagsChanged = getChanges(this.oldFlags, flags);
L2436: const traitsChanged = getChanges(this.evaluationContext.identity?.traits, userTraits);
L2437: 
L2438: if (identifier || Object.keys(userTraits).length) {
L2439: 
L2440: this.evaluationContext.identity = {
L2441: 
L2442: ...this.evaluationContext.identity,
L2443: 
L2444: traits: userTraits,
L2445: 
L2446: };
L2447: 
L2448: if (identifier) {
L2449: 
L2450: this.evaluationContext.identity.identifier = identifier;
L2451: 
L2452: this.identity = identifier;
L2453: 
L2454: }
L2455: 
L2456: }
L2457: 
L2458: this.flags = flags;
L2459: 
L2460: this.updateStorage();
L2461: 
L2462: this._onChange(this.oldFlags, {
L2463: 
L2464: isFromServer: true,
L2465: 
L2466: flagsChanged,
L2467: 
L2468: traitsChanged
L2469: }, this._loadedState(null, FlagSource.SERVER));
L2470: 
L2471: if (this.datadogRum) {
L2472: 
L2473: try {
L2474: 
L2475: if (this.datadogRum!.trackTraits) {
L2476: 
L2477: const traits: Parameters&lt;IDatadogRum[&quot;client&quot;][&quot;setUser&quot;]&gt;[&quot;0&quot;] = {};
L2478: 
L2479: Object.keys(this.evaluationContext.identity?.traits || {}).map((key) =&gt; {
L2480: 
L2481: traits[FLAGSMITH_TRAIT_ANALYTICS_KEY + key] = this.getTrait(key);
L2482: 
L2483: });
L2484: 
L2485: const datadogRumData = {
L2486: 
L2487: ...this.datadogRum.client.getUser(),
L2488: 
L2489: id: this.datadogRum.client.getUser().id || this.evaluationContext.identity?.identifier,
L2490: 
L2491: ...traits,
L2492: 
L2493: };
L2494: this.log(&quot;Setting Datadog user&quot;, datadogRumData);
L2495: 
L2496: this.datadogRum.client.setUser(datadogRumData);
L2497: 
L2498: }
L2499: 
L2500: } catch (e) {
L2501: 
L2502: console.error(e)
L2503: 
L2504: }
L2505: 
L2506: }
L2507: 
L2508: if (this.dtrum) {
L2509: 
L2510: try {
L2511: 
L2512: const traits: DynatraceObject = {
L2513: 
L2514: javaDouble: {},
L2515: 
L2516: date: {},
L2517: 
L2518: shortString: {},
L2519: 
L2520: javaLongOrObject: {},
L2521: 
L2522: }
L2523: 
L2524: Object.keys(this.flags).map((key) =&gt; {
L2525: 
L2526: setDynatraceValue(traits, FLAGSMITH_CONFIG_ANALYTICS_KEY + key, this.getValue(key, { skipAnalytics: true }))
L2527: setDynatraceValue(traits, FLAGSMITH_FLAG_ANALYTICS_KEY + key, this.hasFeature(key, { skipAnalytics: true }))
L2528: 
L2529: })
L2530: 
L2531: Object.keys(this.evaluationContext.identity?.traits || {}).map((key) =&gt; {
L2532: 
L2533: setDynatraceValue(traits, FLAGSMITH_TRAIT_ANALYTICS_KEY + key, this.getTrait(key))
L2534: 
L2535: })
L2536: 
L2537: this.log(&quot;Sending javaLongOrObject traits to dynatrace&quot;, traits.javaLongOrObject)
L2538: 
L2539: this.log(&quot;Sending date traits to dynatrace&quot;, traits.date)
L2540: 
L2541: this.log(&quot;Sending shortString traits to dynatrace&quot;, traits.shortString)
L2542: this.log(&quot;Sending javaDouble to dynatrace&quot;, traits.javaDouble)
L2543: 
L2544: // @ts-expect-error
L2545: 
L2546: this.dtrum.sendSessionProperties(
L2547: 
L2548: traits.javaLongOrObject, traits.date, traits.shortString, traits.javaDouble
L2549: 
L2550: )
L2551: 
L2552: } catch (e) {
L2553: 
L2554: console.error(e)
L2555: 
L2556: }
L2557: 
L2558: }
L2559: 
L2560: };
L2561: 
L2562: if (evaluationContext.identity) {
L2563: 
L2564: return Promise.all([
L2565: 
L2566: (evaluationContext.identity.traits &amp;&amp; Object.keys(evaluationContext.identity.traits).length) || !evaluationContext.identity.identifier ?
L2567: 
L2568: this.getJSON(api + &#39;identities/&#39;, &quot;POST&quot;, JSON.stringify({
L2569: &quot;identifier&quot;: evaluationContext.identity.identifier,
L2570: 
L2571: &quot;transient&quot;: evaluationContext.identity.transient,
L2572: 
L2573: traits: Object.entries(evaluationContext.identity.traits!).map(([tKey, tContext]) =&gt; {
L2574: 
L2575: return {
L2576: 
L2577: trait_key: tKey,
L2578: 
L2579: trait_value: tContext?.value,
L2580: 
L2581: transient: tContext?.transient,
L2582: 
L2583: }
L2584: 
L2585: }).filter((v) =&gt; {
L2586: 
L2587: if (typeof v.trait_value === &#39;undefined&#39;) {
L2588: 
L2589: this.log(&quot;Warning - attempted to set an undefined trait value for key&quot;, v.trait_key)
L2590: 
L2591: return false
L2592: 
L2593: }
L2594: 
L2595: return true
L2596: 
L2597: })
L2598: 
L2599: })) :
L2600: this.getJSON(api + &#39;identities/?identifier=&#39; + encodeURIComponent(evaluationContext.identity.identifier) + (evaluationContext.identity.transient ? &#39;&amp;transient=true&#39; : &#39;&#39;)),
L2601: 
L2602: ])
L2603: 
L2604: .then((res) =&gt; {
L2605: 
L2606: this.evaluationContext.identity = {...this.evaluationContext.identity, traits: {}}
L2607: 
L2608: return handleResponse(res?.[0] as IFlagsmithResponse | null)
L2609: 
L2610: }).catch(({ message }) =&gt; {
L2611: 
L2612: const error = new Error(message)
L2613: 
L2614: return Promise.reject(error)
L2615: 
L2616: });
L2617: 
L2618: } else {
L2619: 
L2620: return this.getJSON(api + &quot;flags/&quot;)
L2621: 
L2622: .then((res) =&gt; {
L2623: return handleResponse({ flags: res as IFlagsmithResponse[&#39;flags&#39;], traits:undefined })
L2624: 
L2625: })
L2626: 
L2627: }
L2628: 
L2629: };
L2630: 
L2631: analyticsFlags = () =&gt; {
L2632: 
L2633: const { api } = this;
L2634: 
L2635: if (!this.evaluationEvent || !this.evaluationContext.environment || !this.evaluationEvent[this.evaluationContext.environment.apiKey]) {
L2636: 
L2637: return
L2638: 
L2639: }
L2640: 
L2641: if (this.evaluationEvent &amp;&amp; Object.getOwnPropertyNames(this.evaluationEvent).length !== 0 &amp;&amp; Object.getOwnPropertyNames(this.evaluationEvent[this.evaluationContext.environment.apiKey]).length !== 0) {
L2642: return this.getJSON(api + &#39;analytics/flags/&#39;, &#39;POST&#39;, JSON.stringify(this.evaluationEvent[this.evaluationContext.environment.apiKey]))
L2643: 
L2644: .then((res) =&gt; {
L2645: 
L2646: if (!this.evaluationContext.environment) {
L2647: 
L2648: return;
L2649: 
L2650: }
L2651: 
L2652: const state = this.getState();
L2653: 
L2654: if (!this.evaluationEvent) {
L2655: 
L2656: this.evaluationEvent = {}
L2657: 
L2658: }
L2659: 
L2660: this.evaluationEvent[this.evaluationContext.environment.apiKey] = {}
L2661: 
L2662: this.setState({
L2663: 
L2664: ...state,
L2665: 
L2666: evaluationEvent: this.evaluationEvent,
L2667: 
L2668: });
L2669: 
L2670: this.updateEventStorage();
L2671: 
L2672: }).catch((err) =&gt; {
…[truncated]</excerpt>
</source>
<source kind="open_page">
<title>flagsmith-js-client/types.d.ts at main · Flagsmith/flagsmith-js-client · GitHub</title>
<location>https://github.com/Flagsmith/flagsmith-js-client/blob/main/types.d.ts</location>
<excerpt>citeturn1view1 [wordlim: 200] Crawled: last month; Content type: text/html; Source: open({&quot;ref_id&quot;:&quot;turn0search3&quot;,&quot;lineno&quot;:null}); Total lines: 1629
L1438: ): IFlagsmithValue&lt;FValue&lt;F, K&gt;&gt;;
L1439: 
L1440: /**
L1441: 
L1442: * Get the value of a particular trait for the identified user
L1443: 
L1444: */
L1445: 
L1446: getTrait: (key: T) =&gt; IFlagsmithValue;
L1447: 
L1448: /**
L1449: 
L1450: * Get the values of all traits for the identified user
L1451: 
L1452: */
L1453: 
L1454: getAllTraits: () =&gt; Record&lt;string, IFlagsmithValue&gt;;
L1455: 
L1456: /**
L1457: 
L1458: * Set a specific trait for a given user id, triggers a call to get flags
L1459: 
L1460: * */
L1461: 
L1462: setTrait: (key: T, value: IFlagsmithTrait) =&gt; Promise&lt;void&gt;;
L1463: 
L1464: /**
L1465: 
L1466: * Set a key value set of traits for a given user, triggers a call to get flags
L1467: 
L1468: */
L1469: setTraits: (traits: ITraits) =&gt; Promise&lt;void&gt;;
L1470: 
L1471: /**
L1472: 
L1473: * Record an arbitrary product event (e.g. &quot;purchase&quot;). No-op when events
L1474: 
L1475: * are disabled (enableEvents is not set). identifier/traits default to the
L1476: 
L1477: * current identified context when omitted.
L1478: 
L1479: * @experimental @internal
L1480: 
L1481: */
L1482: 
L1483: trackEvent: (event: string, opts?: {
L1484: 
L1485: identifier?: string | null;
L1486: 
L1487: value?: IFlagsmithValue;
L1488: 
L1489: traits?: ITraits;
L1490: 
L1491: metadata?: Record&lt;string, unknown&gt;;
L1492: 
L1493: }) =&gt; void;
L1494: 
L1495: /**
L1496: * Record that an identity was exposed to a flag/variant (emits the reserved
L1497: 
L1498: * &quot;$flag_exposure&quot; event). No-op when events are disabled (enableEvents is
L1499: 
L1500: * not set). Skipped (with a log) when no identifier resolves — identify()
L1501: 
L1502: * first (optionally with transient: true) or pass opts.identifier.
L1503: 
L1504: * @experimental @internal
L1505: 
L1506: */
L1507: 
L1508: trackExposureEvent: (featureName: string, opts?: {
L1509: 
L1510: identifier?: string | null;
L1511: 
L1512: value?: IFlagsmithValue;
L1513: 
L1514: traits?: ITraits;
L1515: 
L1516: metadata?: Record&lt;string, unknown&gt;;
L1517: 
L1518: }) =&gt; void;
L1519: 
L1520: /**
L1521: * Resolve a flag for the currently identified user and fire one
L1522: 
L1523: * &quot;$flag_exposure&quot; event with the selected variant as its value (skipped
L1524: 
L1525: * unless flags were loaded from the server and the flag has a variant).
L1526: 
L1527: * When events are disabled (enableEvents is not set) this degrades to a
L1528: 
L1529: * plain flag read.
L1530: 
L1531: * @experimental @internal
L1532: 
L1533: */
L1534: 
L1535: getExperimentFlag: (featureName: string) =&gt; IFlagsmithFeature | null;
L1536: 
L1537: /**
L1538: 
L1539: * Force-flush any buffered events. Useful server-side/SSR where the timer
L1540: * may not fire before the request ends. No-op when events are disabled.
L1541: 
L1542: * @experimental @internal
L1543: 
L1544: */
L1545: 
L1546: flushEvents: () =&gt; Promise&lt;void&gt;;
L1547: 
L1548: /**
L1549: 
L1550: * Whether the events pipeline is enabled (enableEvents: true was passed
L1551: 
L1552: * to init). Used by the React useExperiment hook to skip exposure
L1553: 
L1554: * recording when events are off.
L1555: 
L1556: * @experimental @internal
L1557: 
L1558: */
L1559: 
L1560: readonly eventsEnabled: boolean;
L1561: 
L1562: /**
L1563: 
L1564: * The stored identity of the user
L1565: 
L1566: */
L1567: 
L1568: identity?: IIdentity;
L1569: 
L1570: /**
L1571: 
L1572: * Whether the flagsmith SDK is initialised
L1573: 
L1574: */
L1575: initialised?: boolean;
L1576: 
L1577: /**
L1578: 
L1579: * Returns ths current loading state
L1580: 
L1581: */
L1582: 
L1583: loadingState?: LoadingState;
L1584: 
L1585: /**
L1586: 
L1587: * Used internally, this function will callback separately to onChange whenever flags are updated
L1588: 
L1589: */
L1590: 
L1591: _trigger?: () =&gt; void;
L1592: 
L1593: /**
L1594: 
L1595: * Used internally, this function will trigger the useFlagsmithLoading hook when loading state changes
L1596: 
L1597: */
L1598: 
L1599: _triggerLoadingState?: () =&gt; void;
L1600: 
L1601: /**
L1602: 
L1603: * Used internally, this is the cache options provided in flagsmith.init
L1604: 
L1605: */
L1606: 
L1607: cacheOptions: {
L1608: 
L1609: ttl: number;
L1610: 
L1611: skipAPI: boolean;
L1612: loadStale: boolean;
L1613: 
L1614: };
L1615: 
L1616: /**
L1617: 
L1618: * Used internally, this is the api provided in flagsmith.init, defaults to our production API
L1619: 
L1620: */
L1621: 
L1622: api: string
L1623: 
L1624: }
L1625: 
L1626: export {};
L1627: 
L1628: You can’t perform that action at this time.
</excerpt>
</source>
<source kind="open_page">
<title>Set Up an Experiment with a Coding Agent | Flagsmith Docs</title>
<location>https://docs.flagsmith.com/experimentation/set-up-with-a-coding-agent</location>
<excerpt>citeturn1view2 [wordlim: 200] Crawled: today; Content type: text/html; Source: open({&quot;ref_id&quot;:&quot;turn0search0&quot;,&quot;lineno&quot;:null}); Total lines: 230
L80:        If there is no app to change, offer to build a tiny demo instead (a single HTML page or a script) that simulates
L81: 
L82:        users, so results show up within minutes.
L83: 
L84:     2. Check the stack. Read https://docs.flagsmith.com/experimentation/run-an-experiment and follow its tab for this
L85: 
L86:        app&#39;s language from here on; it has the install, initialise, exposure and conversion code for each SDK. The SDKs
L87:        that can send events are JavaScript and React (`@flagsmith/flagsmith` 12.4.0+), Python (`flagsmith` 6.3.0+), Java
L88: 
L89:        (`com.flagsmith:flagsmith-java-client` 8.2.0+), Node.js (`@flagsmith/nodejs` 9.1.0+; the older `flagsmith-nodejs`
L90: 
L91:        package can&#39;t), Go (`flagsmith-go-client/v5` v5.3.0+) and Flutter (`flagsmith` 6.2.0+). If the app uses none of
L92: 
L93:        them, tell me and stop. Find how the app identifies users: exposures and conversions are joined per identity, so
L112: 
L113:        `GET api/v1/environments/&lt;environment key&gt;/warehouse-connections/` returns none, send me to
L114: 
L115:        https://docs.flagsmith.com/experimentation/connect-a-warehouse and stop.
L116: 
L117:     4. Create a multivariate flag: `flagsmith feature create &lt;flag&gt; --value control --enabled`, then add each variation
L118: 
L119:        with `flagsmith feature variant add &lt;flag&gt; --value &lt;value&gt; --key &lt;key&gt; --weight 0`. Every variation needs a key:
L120:        one without a key records no exposures. `control` is reserved for the flag&#39;s own value, and `--variants` on
L121: 
L122:        `feature create` can&#39;t set keys. Check with `flagsmith feature get &lt;flag&gt; --json`.
L123: 
L124:     5. Create the metric: `POST api/v1/environments/&lt;environment key&gt;/experiment-metrics/` with
L125: 
L126:        {&quot;name&quot;: &quot;&lt;name&gt;&quot;, &quot;aggregation&quot;: &quot;occurrence&quot;, &quot;direction&quot;: &quot;up&quot;, &quot;definition&quot;: {&quot;version&quot;: 1, &quot;event&quot;: &quot;&lt;event&gt;&quot;}}
L127:        The event name is case-sensitive and must match what the code sends.
L128: 
L129:     6. Create the experiment: `POST api/v1/environments/&lt;environment key&gt;/experiments/` with
L130: 
L131:        {&quot;feature&quot;: &lt;flag id&gt;, &quot;name&quot;: &quot;&lt;name&gt;&quot;, &quot;hypothesis&quot;: &quot;&lt;hypothesis&gt;&quot;,
L132: 
L133:        &quot;metrics&quot;: [{&quot;metric&quot;: &lt;metric id&gt;, &quot;expected_direction&quot;: &quot;increase&quot;}],
L134: 
L135:        &quot;experiment_rollout&quot;: {&quot;enabled&quot;: true, &quot;rollout_percentage&quot;: 100,
L136: 
L137:        &quot;feature_state_value&quot;: {&quot;type&quot;: &quot;string&quot;, &quot;value&quot;: &quot;control&quot;},
L145:        - Install or upgrade the SDK and initialise it with events enabled, using the environment key the app already
L146: 
L147:          uses for flags. For a local instance, also set the page&#39;s two self-hosted options: the API URL
L148: 
L149:          http://localhost:8000/api/v1/ and the event ingestion service URL http://localhost:8080/.
L150: 
L151:        - Identify the user, then get the experiment flag (for example `getExperimentFlag`, or `useExperiment` in React)
L152:          where the user actually sees the variation, and render by its value.
L153: 
L154:        - Track the conversion event where success happens.
L155: 
L156:        - Send buffered events before the app exits, as the page shows for this SDK.
L157: 
L158:        Keep keys out of source control, following how the app already handles configuration. Once the changes are in,
L159: 
L160:        start the experiment with `POST api/v1/environments/&lt;environment key&gt;/experiments/&lt;id&gt;/start/` and check its
L161: 
L162:        status is `running`.
L163: 
L164:     8. Offer to send test traffic with a short script that simulates a few hundred users, each with its own identifier:
L165: 
L166:        get the experiment flag for each one and track the event for some of them. The SDKs send events in batches, so
L167: 
L168:        send what is left before the script exits: JavaScript `await flushEvents()`, Python
L169: 
L170:        `EventProcessorConfig(max_buffer_items=1)`, Node.js `await close()`, Java `close()`, Go `FlushEvents(ctx)`,
L171:        Flutter `await flushEvents()`. About a minute later, call
L172: 
L173:        `POST api/v1/environments/&lt;environment key&gt;/experiments/&lt;id&gt;/results/refresh/` (allowed once every 5 minutes) and
L174: 
L175:        read `GET .../experiments/&lt;id&gt;/results/`. Finish with a link to the experiment:
L176: 
L177:        &lt;dashboard&gt;/project/&lt;project id&gt;/environment/&lt;environment key&gt;/experiments/&lt;id&gt;, where the dashboard is
L178: 
L179:        https://app.flagsmith.com on Cloud or http://localhost:8000 locally.
L180: 
L181:     `
L182: ## After it runs【31†​】
L183: 
L184: Open the experiment in the dashboard to watch exposures arrive and read the results; see 【16†Analyse an Experiment】. The flag is locked while the experiment runs.
L185: 
L186: If the agent stops on an error, the most common causes are:
L187:   * No exposures recorded: a variation has no key, the user isn&#39;t identified before the experiment flag is requested, or the process exited before buffered events were sent.
L188:   * Conversions not counted: the event name doesn&#39;t exactly match the metric&#39;s event, or the event was sent with a different identifier from the exposure.
L189:   * Events not arriving on a local instance: the SDK is still sending to Flagsmith Cloud. Set the event ingestion service URL to `http://localhost:8080/`.
L190: See 【32†Troubleshooting】 for more checks.
L191: 
L192: 【9†Previous Experimentation】【11†Next Connect a Warehouse】
L193: 
L194:   * 【27†Prerequisites】
L195:   * 【30†The prompt】
L196:   * 【31†After it runs】
L197: 
L198: Flagsmith
L199: 
L200:   * 【33†Flagsmith.com†flagsmith.com】
L201:   * 【3†X†x.com】
L202: 
L203: Open Source
L204: 
L205:   * 【34†GitHub†github.com】
L206:   * 【35†Discord†discord.gg】
L207: 
L208: More
L209: 
L210:   * 【36†Blog†flagsmith.com】
L211:   * 【37†Podcast†flagsmith.com】
L212:   * 【38†Status and Uptime†status.flagsmith.com】
L213: 
L214: How-to guides
L215:   * 【39†.NET†www.flagsmith.com】
L216:   * 【40†Angular†www.flagsmith.com】
L217:   * 【41†Flutter†www.flagsmith.com】
L218:   * 【42†Golang†www.flagsmith.com】
L219:   * 【43†Java†www.flagsmith.com】
L220:   * 【44†JavaScript†www.flagsmith.com】
L221:   * 【45†Kotlin/Android†www.flagsmith.com】
L222:   * 【46†Node.js†www.flagsmith.com】
L223:   * 【47†PHP†www.flagsmith.com】
L224:   * 【48†Python†www.flagsmith.com】
L225:   * 【49†React Native†www.flagsmith.com】
L226:   * 【50†Ruby†www.flagsmith.com】
L227:   * 【51†Spring Boot†www.flagsmith.com】
L228:   * 【52†Swift/iOS†www.flagsmith.com】
</excerpt>
</source>
<source kind="search">
<title>flagsmith-js-client/types.d.ts at main · Flagsmith/flagsmith-js-client · GitHub</title>
<location>https://github.com/Flagsmith/flagsmith-js-client/blob/main/types.d.ts</location>
<excerpt>citeturn0search3 [wordlim: 200] Crawled: 3 weeks ago; trait_value: IFlagsmithValue;

transient?: boolean; ... * are disabled (enableEvents is not set). identifier/traits default to the ... * &quot;$flag_exposure&quot; event). ... * the flag&#39;s experiment, i.e. `flag.experiment.inExperiment` is true). ... * Whether the flagsmith SDK is initialised
onChange?: OnChange&lt;FKey&lt;F&gt;&gt;;

onError?: (err: Error) =&gt; void;

preventFetch?: boolean;

state?: IState;

_trigger?: () =&gt; void;

_triggerLoadingState?: () =&gt; void;

/**

* Customer application metadata

*/

applicationMetadata?: ApplicationMetadata;

/**

* @experimental Internal use only — API may change without notice.

* Opt-in gate for the events pipeline. When true, an EventProcessor is

* started and trackEvent / trackExposureEvent / getExperimentFlag become

* available.

* @internal

*/

enableEvents?: boolean;

/**

* @experimental Optional tuning for the events pipeline. Only valid when

* enableEvents is true (passing it without enableEvents throws at init).

* @internal

*/

eventProcessorConfig?: {

eventsApiUrl?: string;

maxBuffer?: number;

flushInterval?: number;

};

}

export interface IFlagsmithResponse {

identifier?: string,

traits?: {

trait_key: string;

trait_value: IFlagsmithValue;

transient?: boolean;

}[];

flags?: {

enabled: boolean;

feature_state_value: IFlagsmithValue;

variant?: string;

/**

* Record an arbitrary product event (e.g. &quot;purchase&quot;). No-op when events

* are disabled (enableEvents is not set). identifier/traits default to the

* current identified context when omitted.

* @experimental @internal

*/

trackEvent: (event: string, opts?: {

identifier?: string | null;

value?: IFlagsmithValue;

traits?: ITraits;

metadata?: Record&lt;string, unknown&gt;;

}) =&gt; void;

/**

* Record that an identity was exposed to a flag/variant (emits the reserved

* &quot;$flag_exposure&quot; event). No-op when events are disabled (enableEvents is

* not set). Skipped (with a log) when no identifier resolves — identify()

* first (optionally with transient: true) or pass opts.identifier.

* @experimental @internal

*/

trackExposureEvent: (featureName: string, opts?: {

identifier?: string | null;

value?: IFlagsmithValue;

traits?: ITraits;

metadata?: Record&lt;string, unknown&gt;;

}) =&gt; void;

/**

* Resolve a flag for the currently identified user and fire one

* &quot;$flag_exposure&quot; event with the selected variant as its value (skipped

* unless flags were loaded from the server and the identity is enrolled in

* the flag&#39;s experiment, i.e. `flag.experiment.inExperiment` is true).

* When events are disabled (enableEvents is not set) this degrades to a

* plain flag read.

* @experimental @internal

*/

getExperimentFlag: (featureName: string) =&gt; IFlagsmithFeature | null;

/**

* Force-flush any buffered events. Useful server-side/SSR where the timer

* may not fire before the request ends. No-op when events are disabled.

* @experimental @internal

*/

flushEvents: () =&gt; Promise&lt;void&gt;;

/**

* Whether the events pipeline is enabled (enableEvents: true was passed

* to init). Used by the React useExperiment hook to skip exposure

* recording when events are off.

* @experimental @internal

*/

readonly eventsEnabled: boolean;

/**

* The stored identity of the user

*/

identity?: IIdentity;

/**

* Whether the flagsmith SDK is initialised

*/

initialised?: boolean;

/**

* Returns ths current loading state

*/

loadingState?: LoadingState;

/**

* Used internally, this function will callback separately to onChange whenever flags are updated

*/

_trigger?: () =&gt; void;

/**

* Used internally, this function will trigger the useFlagsmithLoading hook when loading state changes

*/

_triggerLoadingState?: () =&gt; void;

/**

* Used internally, this is the cache options provided in flagsmith.init

*/

cacheOptions: {

ttl: number;

skipAPI: boolean;

loadStale: boolean;

};

/**

* Used internally, this is the api provided in flagsmith.init, defaults to our production API

*/

api: string

}

export {};</excerpt>
</source>
<source kind="search">
<title>Set Up an Experiment with a Coding Agent | Flagsmith Docs</title>
<location>https://docs.flagsmith.com/experimentation/set-up-with-a-coding-agent</location>
<excerpt>citeturn0search0 [wordlim: 200] Crawled: last week; Read https://docs.flagsmith.com/experimentation/run-an-experiment and follow its tab for this ...        that can send events are JavaScript and React (`@flagsmith/flagsmith` 12.4.0+), Python (`flagsmith` 6.3.0+), Java ... Create the metric: `POST api/v1/environments/&lt;environment key&gt;/experiment-metrics/` with ...   * No exposures recorded: a variation has no key, the user isn&#39;t identified before the experiment flag is requested, or the process exited before buffered events were sent.
  * Conversions not counted: the event name doesn&#39;t exactly match the metric&#39;s event, or the event was sent with a different identifier from the exposure.
  * Events not arriving on a local instance: the SDK is still sending to Flagsmith Cloud.
    
       users, so results show up within minutes.
    
    2. Check the stack. Read https://docs.flagsmith.com/experimentation/run-an-experiment and follow its tab for this
    
       app&#39;s language from here on; it has the install, initialise, exposure and conversion code for each SDK. The SDKs
    
       that can send events are JavaScript and React (`@flagsmith/flagsmith` 12.4.0+), Python (`flagsmith` 6.3.0+), Java
    
       (`com.flagsmith:flagsmith-java-client` 8.2.0+), Node.js (`@flagsmith/nodejs` 9.1.0+; the older `flagsmith-nodejs`
    
       package can&#39;t), Go (`flagsmith-go-client/v5` v5.3.0+) and Flutter (`flagsmith` 6.2.0+). If the app uses none of
    
       them, tell me and stop. Find how the app identifies users: exposures and conversions are joined per identity, so
    
       both must use the same identifier.
    
    3. Connect the Flagsmith CLI. If `flagsmith --version` doesn&#39;t report v2 or later, install it with
    
       `curl -fsSL https://get.flagsmith.com | sh` or `brew install Flagsmith/tap/flagsmith`; use

## After it runs​

Open the experiment in the dashboard to watch exposures arrive and read the results; see Analyse an Experiment. The flag is locked while the experiment runs.

If the agent stops on an error, the most common causes are:

  * No exposures recorded: a variation has no key, the user isn&#39;t identified before the experiment flag is requested, or the process exited before buffered events were sent.
  * Conversions not counted: the event name doesn&#39;t exactly match the metric&#39;s event, or the event was sent with a different identifier from the exposure.
  * Events not arriving on a local instance: the SDK is still sending to Flagsmith Cloud. Set the event ingestion service URL to `http://localhost:8080/`.

See Troubleshooting for more checks.</excerpt>
</source>
<omitted_sources count="21">Additional source records were omitted from this bounded result.</omitted_sources>
</source_evidence>

Citations:

- 1: https://github.com/Flagsmith/flagsmith-js-client/blob/main/types.d.ts
- 2: https://github.com/Flagsmith/flagsmith-js-client/blob/main/types.d.ts

Script executed:

set -u
echo "== locate Kotlin sources mentioning the claimed symbols =="
rg -n -F -e 'getExperimentFlag' -e 'trackExposureEvent' -e 'transient' -e 'class Trait' -e 'FlagExposure' -e 'flag_exposure' -- FlagsmithClient/src/main/java || test "$?" -eq 1
echo
echo "== Flagsmith.kt around the reported line, with line numbers =="
nl -ba FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt | sed -n '170,260p'
echo
echo "== Trait declaration(s) =="
rg -n -F -e 'data class Trait' -e 'class Trait' -- FlagsmithClient/src/main/java || test "$?" -eq 1

Repository: Flagsmith/flagsmith-kotlin-android-client

Length of output: 11132


Script executed:

set -u
echo "== Trait.kt declaration =="
nl -ba FlagsmithClient/src/main/java/com/flagsmith/entities/Trait.kt | sed -n '1,14p'
echo
echo "== FlagsmithEventProcessor.kt, full file with line numbers =="
nl -ba FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithEventProcessor.kt

Repository: Flagsmith/flagsmith-kotlin-android-client

Length of output: 6815


Filter transient traits before they go to the events API.

getExperimentFlag builds the exposure event's traits map from every Trait in the list and never checks Trait.transient. The map keeps only each trait's key and value, so the transient marker is lost. The exposure event is then sent in a batch to v1/events.

The SDK already sends transient as a per-trait marker to the identity and trait endpoints. This shows that a transient trait is meant not to be persisted. The exposure path should apply the same rule.

🛡️ Proposed fix
--- "a/FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt"
+++ "b/FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt"
@@ -219,7 +219,7 @@
                     featureName,
                     identity = identity,
                     value = flag.variant,
-                    traits = traits?.associate { it.key to it.traitValue },
+                    traits = traits?.filterNot { it.transient }?.associate { it.key to it.traitValue },
                     metadata = mapOf("experiment_id" to experiment.id)
                 )
             }
Committable suggestion

IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
traits = traits?.associate { it.key to it.traitValue },
traits = traits?.filterNot { it.transient }?.associate { it.key to it.traitValue },

@renovate renovate Bot changed the title feat: Track events and exposures, and get experiment flags (SDK spec v0.6.0) deps: update dependency flagsmith/sdk-spec to v0.7.0 Oct 29, 2026
@renovate
renovate Bot force-pushed the renovate/sdk-spec branch from a6265bd to 99d29cd Compare October 29, 2026 19:54
Implements what SDK spec v0.6.0 requires:

- `enableEvents` / `eventsBaseUrl` / `eventsFlushPeriod` options, sending
  buffered events in batches to Flagsmith's events API (`v1/events`).
- `trackEvent` and `trackExposureEvent`, the latter recorded as `$flag_exposure`.
- `getExperimentFlag`, which records an exposure to the served variant when
  the identity is enrolled in a running experiment.
- `Flag.variant` and `Flag.metadata.experiment`, as returned by the API.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit a6265bd)
@renovate

renovate Bot commented Nov 1, 2026

Copy link
Copy Markdown
Author

Edited/Blocked Notification

Renovate will not automatically rebase this PR, because it does not recognize the last commit author and assumes somebody else may have edited the PR.

You can manually request rebase by checking the rebase/retry box above.

⚠️ Warning: custom changes will be lost.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

sdk-spec Bumps of the Flagsmith SDK behavioural spec

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants