Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: unleash implementation #15263

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

mc-jones
Copy link
Contributor

@mc-jones mc-jones commented Feb 27, 2025

The type of this PR is: Refactor

This PR solves PHIRE-1620

Description

This PR refactors the existing unleash implementation with the following goals:

  • Simplify the code
  • Use the unleash-proxy-client to retrieve flags and variants when in a browser context. This replaces the existing approach of eagerly loading all flags in express middleware on every request and using sharify to expose them to both server and client.

Motivations

  • Eagerly loading flags and variants breaks metric collection, making observability and maintenance difficult. For example, we are unable to use the unleash admin UI to identify stale flags.
  • The code was unnecessarily complicated. For example, it wrapped the unleash node SDK in a custom class, introducing an unnecessary level of abstraction. The idea of making it easier to swap in different flag provider easily, while well intentioned, has proven not worth the tradeoff.
  • Using the canonical implementation allows us to rely on unleash's documentation, removing some additional maintenance.

Considerations

  • This is currently configured to use the frontend api, which may be sufficient for our use case. The api is the exact same as unleash edge, so if we spin up an edge instance, we just need to change the UNLEASH_URL to point to the instance. I'll need to investigate this a bit more.

Usage

When working client side (in react), developers can follow the unleash "how to use" instructions:

import { useFlag, useVariant } from '@unleash/proxy-client-react';

// Check toggle
const TestComponent = () => {
  const enabled = useFlag('demo-feature');

  if (enabled) {
    return <SomeComponent />;
  }
  return <AnotherComponent />;
};

// Check variant
const TestComponent = () => {
  const variant = useVariant('demo-variant');

  if (variant.enabled && variant.name === 'SomeComponent') {
    return <SomeComponent />;
  } else if (variant.enabled && variant.name === 'AnotherComponent') {
    return <AnotherComponent />;
  }
  return <DefaultComponent />;
};

When working server side (in node), developers just need to retrieve the client first using the getOrInitUnleashServerClient function, and then follow the "how to use" instructions:

import { getOrInitUnleashServerClient } from "Server/featureFlags/unleashHelpers"

// Get existing unleash client
const unleash = getOrInitUnleashServerClient()

// Setup context
// NOTE: developers will need to handle passing in the context
// when retrieving flags/variant while working server side. This 
// was done for them in the previous implementation. 
const context = {
  userId: res.locals.sd.CURRENT_USER,
  sessionId: res.locals.sd.SESSION_ID,
};

// Check toggle
if (unleash.isEnabled('demo', context)) {
  console.log('Toggle enabled');
} else {
  console.log('Toggle disabled');
}

// Check variant
const variant = unleash.getVariant('demo-variant', context);

if (variant.name === 'experiment') {
  // do something with the experiment variant...
}

TODO

  • Push up modified env
  • Spike on using unleash edge (non-blocking)

I haven't pushed these yet because I wanted the implementation changes to be easier to review:

  • Migrate existing flags and experiments
  • Refactor test code

@artsy-peril
Copy link
Contributor

artsy-peril bot commented Feb 27, 2025

unleash-proxy-client

Author: @Unleash

Description: A browser client that can be used together with Unleash Edge or the Unleash Frontend API.

Homepage: /~https://github.com/unleash/unleash-proxy-client-js#readme

Createdover 5 years ago
Last Updated28 days ago
LicenseApache-2.0
Maintainers3
Releases94
Direct Dependenciestiny-emitter and uuid
This README is too long to show.

@unleash/proxy-client-react

Author: Unknown

Description: React interface for working with unleash

Homepage: /~https://github.com/Unleash/unleash-proxy-react#readme

Createdover 3 years ago
Last Updated23 days ago
LicenseApache-2.0
Maintainers6
Releases73
Keywordsunleash, react and feature-toggles
This README is too long to show.

New dependencies added: @unleash/proxy-client-react and unleash-proxy-client.

Generated by 🚫 dangerJS against 85138a0

@@ -115,7 +114,7 @@ export const TOOLS_URL: any = "https://tools.artsy.net"
export const TRACK_PAGELOAD_PATHS: any = null
export const UNLEASH_API: any = null
export const UNLEASH_APP_NAME: any = null
export const UNLEASH_INSTANCE_ID: any = null
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wasn't used

@@ -93,6 +93,7 @@
"@styled-system/theme-get": "5.1.2",
"@swc-node/register": "^1.10.9",
"@swc/helpers": "^0.5.15",
"@unleash/proxy-client-react": "^4.5.2",
Copy link
Contributor Author

@mc-jones mc-jones Feb 27, 2025

Choose a reason for hiding this comment

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

In case it saves others some confusion, I want to clarify a non-obvious point. While the unleash proxy is deprecated, the proxy-clients are not. They are still necessary to interface with our self-hosted unleash instances. So we can now spin down the artsy-unleash-web-proxy deployment and replace it with unleash edge or just point request directly to our web deployment

@@ -178,7 +179,8 @@
"superagent": "3.8.3",
"tti-polyfill": "0.2.2",
"underscore.string": "3.3.5",
"unleash-client": "^3.12.0",
"unleash-client": "^6.5.0",
"unleash-proxy-client": "^3.7.3",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Both unleash-proxy-client and @unleash/proxy-client-react are needed.

Comment on lines +9 to +20
export function getOrInitUnleashServerClient(): Promise<Unleash> {
const config = {
url: UNLEASH_API,
appName: UNLEASH_APP_NAME,
refreshInterval: 10000,
environment: NODE_ENV,
customHeaders: {
Authorization: UNLEASH_SERVER_KEY,
},
}

return startUnleash(config)
Copy link
Contributor Author

@mc-jones mc-jones Feb 27, 2025

Choose a reason for hiding this comment

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

This should only ever return a single instance of the Unleash client since we're using startUnleash, which implements a singleton. I'll monitor this in staging, though.

Also, I acknowledge the name is a lot, but I wanted to clearly differentiate from the frontend proxy client. So I didn't want to use useUnleashServerClient, for example, since prepending a name with use is a react hook convention.

Comment on lines +68 to +79
const unleashConfig = {
url: `${sd.UNLEASH_API}frontend/`,
clientKey: sd.UNLEASH_CLIENT_KEY,
refreshInterval: 15,
appName: sd.UNLEASH_APP_NAME,
environment: sd.NODE_ENV,
context: {
userId: props.user?.id,
sessionId: sd.SESSION_ID,
},
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Considered moving this into its own file, but prefer it here. If you have strong opinions, LMK.

Copy link
Member

@damassi damassi Feb 28, 2025

Choose a reason for hiding this comment

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

A common pattern is to keep everything self-contained in our own component, added somewhere in the system directory (with other feature flag stuff):

import { FlagProvider } from '...'

const FeatureFlagProvider: React.FC<React.PropsWithChildren> = ({ children }) => {
  const { user } = useSystemContext() 
  
  const unleashConfig = {
    url: `${getENV("UNLEASH_API")}frontend/`,
    clientKey: getENV("UNLEASH_CLIENT_KEY"),
    refreshInterval: 15,
    appName: getENV("UNLEASH_APP_NAME"),
    environment: getENV("NODE_ENV"),
    context: {
      userId: user?.id,
      sessionId: getENV("SESSION_ID"),
    },
  }
  
  return <FlagProvider ... >{children}></FlagProvider> 
}

Then import ☝️ in boot


return variant
}

export function useTrackFeatureVariant({
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Variant tracking will remain unchanged.

@@ -84,6 +83,7 @@ export const bootstrapSharify = () => {
"SEGMENT_AMP_WRITE_KEY",
"SEGMENT_WRITE_KEY",
"SENTRY_PUBLIC_DSN",
"SESSION_ID",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was surprised this wasn't already in here. I know we have access to it after we the app boots (in this use case its needed before boot), but I couldn't find exactly where that happens.

@mc-jones mc-jones self-assigned this Feb 28, 2025
@mc-jones mc-jones requested a review from joeyAghion February 28, 2025 12:52
Copy link
Member

@damassi damassi left a comment

Choose a reason for hiding this comment

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

This is such a nice simplifying PR 👍

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

Successfully merging this pull request may close these issues.

2 participants