Skip to content

feat: make Unleash session id visible for backend#1935

Merged
stalniy merged 3 commits intoakash-network:mainfrom
jzsfkzm:feature/1867-full-stack-unleash-session
Sep 18, 2025
Merged

feat: make Unleash session id visible for backend#1935
stalniy merged 3 commits intoakash-network:mainfrom
jzsfkzm:feature/1867-full-stack-unleash-session

Conversation

@jzsfkzm
Copy link
Contributor

@jzsfkzm jzsfkzm commented Sep 17, 2025

closes #1867

Summary by CodeRabbit

  • New Features

    • Feature flags now consider session-specific context via the unleash-session-id cookie, enabling more precise and consistent rollouts.
    • The web proxy now forwards unleash-session-id alongside cf_clearance, preserving session-aware behavior across proxied requests.
    • No UI or public API changes; no user action required.
  • Tests

    • Added tests verifying sessionId propagation and cookie-based parsing in feature flag evaluation.

@jzsfkzm jzsfkzm requested a review from a team as a code owner September 17, 2025 13:47
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 17, 2025

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "feat: make Unleash session id visible for backend" accurately and concisely summarizes the main change: exposing the Unleash session id to backend logic. The changes update feature-flags.service to extract the "unleash-session-id" cookie and include sessionId when calling Unleash.isEnabled, and the proxy was updated to forward that cookie. The title is focused, specific, and appropriate for changelog/history scanning.
Linked Issues Check ✅ Passed The PR fulfills the linked issue (#1867) requirements by extracting the "unleash-session-id" cookie from the incoming HTTP context, passing that value as sessionId into Unleash.isEnabled, and updating the proxy to forward the cookie from frontend to backend; tests were added to cover both undefined and cookie-driven scenarios. These changes directly address propagating the same session id between frontend and backend so Unleash can perform consistent session-based rollouts. Based on the provided summaries, the coding objectives of issue #1867 are met.
Out of Scope Changes Check ✅ Passed All modified files and tests are directly related to propagating and consuming the Unleash session id: feature-flags.service.ts adds session extraction and passes sessionId to the client, apps/deploy-web proxy forwards the cookie, and tests were updated to validate behavior. The summary shows no unrelated functional or API changes outside this scope. Therefore there are no apparent out-of-scope changes introduced by this PR.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (5)
apps/deploy-web/src/pages/api/proxy/[...path].ts (1)

15-22: Only set Cookie header when present; avoid sending an empty header

If neither cookie exists, this writes an empty Cookie header. Prefer setting it only when you have values (or delete it). Also consider parsing via the cookie library for robustness.

-const cookiesToForward = [cfClearance, unleashSessionId].filter(Boolean);
-req.headers.cookie = cookiesToForward.join("; ");
+const cookiesToForward = [cfClearance, unleashSessionId].filter(Boolean) as string[];
+if (cookiesToForward.length > 0) {
+  req.headers.cookie = cookiesToForward.join("; ");
+} else {
+  delete req.headers.cookie;
+}

Optional: switch to cookie.parse for correctness with spaces/encoding:

+import { parse as parseCookie } from "cookie";
@@
-const cookies = req.headers.cookie?.split(";").map(c => c.trim());
-const cfClearance = cookies?.find(c => c.startsWith("cf_clearance="));
-const unleashSessionId = cookies?.find(c => c.startsWith("unleash-session-id="));
+const parsed = req.headers.cookie ? parseCookie(req.headers.cookie) : undefined;
+const cfClearance = parsed?.cf_clearance ? `cf_clearance=${parsed.cf_clearance}` : undefined;
+const unleashSessionId = parsed?.["unleash-session-id"] ? `unleash-session-id=${parsed["unleash-session-id"]}` : undefined;
apps/api/src/core/services/feature-flags/feature-flags.service.ts (2)

54-64: More robust cookie parsing (avoid substring/startsWith pitfalls)

Use a cookie parser and a cookie-name constant (without “=”) to avoid whitespace/order issues and accidental matches.

-import type { AppContext } from "@src/core/types/app-context";
+import type { AppContext } from "@src/core/types/app-context";
+import { parse as parseCookie } from "cookie";
@@
-  private readonly UNLEASH_COOKIE_KEY = "unleash-session-id=";
+  private readonly UNLEASH_COOKIE_NAME = "unleash-session-id";
@@
-  private extractSessionId(): string | undefined {
-    const httpContext = this.executionContext.get("HTTP_CONTEXT") as AppContext | undefined;
-    if (!httpContext) return undefined;
-
-    const cookieHeader = httpContext.req.header("cookie");
-    if (!cookieHeader) return undefined;
-
-    const cookies = cookieHeader.split(";").map(c => c.trim());
-    const unleashCookie = cookies.find(c => c.startsWith(this.UNLEASH_COOKIE_KEY));
-    return unleashCookie?.replace(this.UNLEASH_COOKIE_KEY, "");
-  }
+  private extractSessionId(): string | undefined {
+    const httpContext = this.executionContext.get("HTTP_CONTEXT") as AppContext | undefined;
+    if (!httpContext) return undefined;
+    const cookieHeader = httpContext.req.header("cookie");
+    if (!cookieHeader) return undefined;
+    const parsed = parseCookie(cookieHeader);
+    const value = parsed[this.UNLEASH_COOKIE_NAME];
+    return typeof value === "string" && value.length > 0 ? value : undefined;
+  }

18-18: Deduplicate cookie name across apps

This cookie name also appears in the web proxy. Consider exporting a shared constant (e.g., @src/shared/constants/cookies.ts) to prevent drift.

apps/api/src/core/services/feature-flags/feature-flags.service.spec.ts (2)

186-193: Place setup() at the very bottom of the root describe block

Guideline requires setup at the bottom. Move createUnleashMockClient above, so setup remains last.

-  async function setup(input: {
+  // (Move createUnleashMockClient above this function)
+  async function setup(input: {
     config?: Partial<typeof envConfig>;
     createClient?: (config: UnleashConfig) => Unleash;
     currentUser?: { id: string };
     httpClientInfo?: ClientInfoContextVariables["clientInfo"];
     unleashSessionId?: string;
     skipInitialization?: boolean;
   }) {
@@
-  function createUnleashMockClient(input?: { isEnabledFeatureFlag?: (featureFlag: FeatureFlagValue) => boolean }) {
-    return mock<Unleash>({
-      once(event, callback) {
-        if (event === "synchronized") {
-          process.nextTick(callback);
-        }
-        return this as Unleash;
-      },
-      isEnabled: input?.isEnabledFeatureFlag ? input.isEnabledFeatureFlag : () => false
-    });
-  }
+  // place this helper above `setup` so `setup` stays the last item in the describe block

230-231: Avoid any in tests

Replace the as any cast per TS guideline. A simple Record<string, unknown> keeps type safety without resorting to any.

-              }
-            }) as any
+              }
+            }) as Record<string, unknown>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 95b3430 and bede4a5.

📒 Files selected for processing (3)
  • apps/api/src/core/services/feature-flags/feature-flags.service.spec.ts (3 hunks)
  • apps/api/src/core/services/feature-flags/feature-flags.service.ts (4 hunks)
  • apps/deploy-web/src/pages/api/proxy/[...path].ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Never use type any or cast to type any. Always define the proper TypeScript types.

Files:

  • apps/deploy-web/src/pages/api/proxy/[...path].ts
  • apps/api/src/core/services/feature-flags/feature-flags.service.ts
  • apps/api/src/core/services/feature-flags/feature-flags.service.spec.ts
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{js,ts,tsx}: Never use deprecated methods from libraries.
Don't add unnecessary comments to the code

Files:

  • apps/deploy-web/src/pages/api/proxy/[...path].ts
  • apps/api/src/core/services/feature-flags/feature-flags.service.ts
  • apps/api/src/core/services/feature-flags/feature-flags.service.spec.ts
**/*.spec.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/no-jest-mock.mdc)

Don't use jest.mock() to mock dependencies in test files. Instead, use jest-mock-extended to create mocks and pass mocks as dependencies to the service under test.

**/*.spec.{ts,tsx}: Use setup function instead of beforeEach in test files
setup function must be at the bottom of the root describe block in test files
setup function creates an object under test and returns it
setup function should accept a single parameter with inline type definition
Don't use shared state in setup function
Don't specify return type of setup function

Files:

  • apps/api/src/core/services/feature-flags/feature-flags.service.spec.ts
🧬 Code graph analysis (2)
apps/api/src/core/services/feature-flags/feature-flags.service.ts (2)
apps/api/src/auth/services/auth.service.ts (2)
  • currentUser (12-14)
  • currentUser (16-19)
apps/api/src/user/controllers/user/user.controller.ts (1)
  • httpContext (31-33)
apps/api/src/core/services/feature-flags/feature-flags.service.spec.ts (3)
apps/api/src/auth/services/auth.service.ts (2)
  • currentUser (12-14)
  • currentUser (16-19)
apps/api/src/middlewares/clientInfoMiddleware.ts (1)
  • ClientInfoContextVariables (7-13)
apps/api/src/core/services/feature-flags/feature-flags.ts (1)
  • FeatureFlags (1-5)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
🔇 Additional comments (2)
apps/api/src/core/services/feature-flags/feature-flags.service.ts (1)

35-44: LGTM: sessionId now included in Unleash context

Passing sessionId into the Unleash context aligns with per-session rollout semantics and keeps backwards compatibility when absent.

apps/api/src/core/services/feature-flags/feature-flags.service.spec.ts (1)

115-154: LGTM: covers cookie-based sessionId propagation

Good assertion that sessionId from the Unleash cookie reaches the Unleash client context.

@jzsfkzm jzsfkzm force-pushed the feature/1867-full-stack-unleash-session branch from bede4a5 to 4c4eebb Compare September 17, 2025 17:15
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
apps/api/src/core/services/feature-flags/feature-flags.service.ts (1)

35-45: Session stickiness: OK; confirm FE/BE context parity.

Propagating sessionId into Unleash context solves the rollout stickiness objective. Confirm whether we must keep FE and BE contexts identical; if yes, consider dropping remoteAddress/userAgent/fingerprint/nodeEnv to prevent divergence-driven toggling.

🧹 Nitpick comments (2)
apps/api/src/core/services/feature-flags/feature-flags.service.ts (2)

18-18: Don’t bake “=” into the cookie constant; minor parsing hardening.

Storing the cookie name without “=” simplifies matching and avoids subtle bugs. Also handle empty values.

-  private readonly UNLEASH_COOKIE_KEY = "unleash-session-id=";
+  private readonly UNLEASH_COOKIE_NAME = "unleash-session-id";
-    const cookies = cookieHeader.split(";").map(c => c.trim());
-    const unleashCookie = cookies.find(c => c.startsWith(this.UNLEASH_COOKIE_KEY));
-    return unleashCookie?.replace(this.UNLEASH_COOKIE_KEY, "");
+    const pairs = cookieHeader.split(";").map(c => c.trim());
+    const match = pairs.find(c => c.startsWith(`${this.UNLEASH_COOKIE_NAME}=`));
+    if (!match) return undefined;
+    const value = match.slice(this.UNLEASH_COOKIE_NAME.length + 1);
+    try {
+      return decodeURIComponent(value) || undefined;
+    } catch {
+      return value || undefined;
+    }

54-65: Extractor is simple and safe; add tiny edge-case guard.

Current code can return an empty string when cookie is present without a value. Treat empty as undefined (covered in the diff above). If you prefer maximum robustness, consider using the tiny “cookie” parser library already common in Node stacks.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bede4a5 and 4c4eebb.

📒 Files selected for processing (3)
  • apps/api/src/core/services/feature-flags/feature-flags.service.spec.ts (3 hunks)
  • apps/api/src/core/services/feature-flags/feature-flags.service.ts (4 hunks)
  • apps/deploy-web/src/pages/api/proxy/[...path].ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/api/src/core/services/feature-flags/feature-flags.service.spec.ts
  • apps/deploy-web/src/pages/api/proxy/[...path].ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Never use type any or cast to type any. Always define the proper TypeScript types.

Files:

  • apps/api/src/core/services/feature-flags/feature-flags.service.ts
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{js,ts,tsx}: Never use deprecated methods from libraries.
Don't add unnecessary comments to the code

Files:

  • apps/api/src/core/services/feature-flags/feature-flags.service.ts
🧬 Code graph analysis (1)
apps/api/src/core/services/feature-flags/feature-flags.service.ts (2)
apps/api/src/auth/services/auth.service.ts (2)
  • currentUser (12-14)
  • currentUser (16-19)
apps/api/src/user/controllers/user/user.controller.ts (1)
  • httpContext (31-33)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
🔇 Additional comments (1)
apps/api/src/core/services/feature-flags/feature-flags.service.ts (1)

6-6: Good: type-only import.

Using a type-only import for AppContext avoids runtime bloat and keeps types explicit.

@jzsfkzm jzsfkzm force-pushed the feature/1867-full-stack-unleash-session branch from 4c4eebb to 1957b8f Compare September 18, 2025 11:47
@stalniy stalniy merged commit ed3c047 into akash-network:main Sep 18, 2025
61 checks passed
stalniy pushed a commit that referenced this pull request Nov 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Properly propagate unleash sessionId between frontend and backend

2 participants

Comments