From 500a9c7df4a215230d89850c9c284e6fa4312875 Mon Sep 17 00:00:00 2001 From: Melinda Fekete Date: Tue, 20 Aug 2024 17:19:12 +0200 Subject: [PATCH 01/13] Update 11 principles docs (#7907) --- .../feature-flag-best-practices.md | 384 ++++++------------ 1 file changed, 121 insertions(+), 263 deletions(-) diff --git a/website/docs/topics/feature-flags/feature-flag-best-practices.md b/website/docs/topics/feature-flags/feature-flag-best-practices.md index 22615bfd5e3e..69ac20ee72d6 100644 --- a/website/docs/topics/feature-flags/feature-flag-best-practices.md +++ b/website/docs/topics/feature-flags/feature-flag-best-practices.md @@ -1,339 +1,197 @@ --- title: "Feature flags: Best practices for building and scaling" +description: "Discover 11 essential principles for building robust, large-scale feature flag systems." +toc_max_heading_level: 2 --- -# Feature flags: Best practices for building and scaling feature flag systems +# Best practices for building and scaling feature flags import Figure from '@site/src/components/Figure/Figure.tsx' -Feature flags, sometimes called feature toggles or feature switches, are a software development technique that allows engineering teams to decouple the release of new functionality from software deployments. With feature flags, developers can turn specific features or code segments on or off at runtime, without the need for a code deployment or rollback. Organizations who adopt feature flags see improvements in all key operational metrics for DevOps: Lead time to changes, mean-time-to-recovery, deployment frequency, and change failure rate. +Feature flags, sometimes called feature toggles or feature switches, are a powerful software development technique that allows engineering teams to decouple the release of new functionality from software deployments. -There are 11 principles for building a large-scale feature flag system. These principles have their roots in distributed systems architecture and pay particular attention to security, privacy, and scale that is required by most enterprise systems. If you follow these principles, your feature flag system is less likely to break under load and will be easier to evolve and maintain. +With feature flags, developers can turn specific features or code segments on or off at runtime without needing a code deployment or rollback. Organizations that adopt feature flags see improvements in key DevOps metrics like lead time to changes, mean time to recovery, deployment frequency, and change failure rate. -These principles are: - -- [Background](#background) -- [1. Enable run-time control. Control flags dynamically, not using config files.](#1-enable-run-time-control-control-flags-dynamically-not-using-config-files) -- [2. Never expose PII. Follow the principle of least privilege.](#2-never-expose-pii-follow-the-principle-of-least-privilege) -- [3. Evaluate flags as close to the user as possible. Reduce latency.](#3-evaluate-flags-as-close-to-the-user-as-possible-reduce-latency) -- [4. Scale Horizontally. Decouple reading and writing flags.](#4-scale-horizontally-decouple-reading-and-writing-flags) -- [5. Limit payloads. Feature flag payload should be as small as possible.](#5-limit-payloads-feature-flag-payload-should-be-as-small-as-possible) -- [6. Design for failure. Favor availability over consistency.](#6-design-for-failure-favor-availability-over-consistency) -- [7. Make feature flags short-lived. Do not confuse flags with application configuration.](#7-make-feature-flags-short-lived-do-not-confuse-flags-with-application-configuration) -- [8. Use unique names across all applications. Enforce naming conventions.](#8-use-unique-names-across-all-applications-enforce-naming-conventions) -- [9. Choose open by default. Democratize feature flag access.](#9-choose-open-by-default-democratize-feature-flag-access) -- [10. Do no harm. Prioritize consistent user experience.](#10-do-no-harm-prioritize-consistent-user-experience) -- [11. Enable traceability. Make it easy to understand flag evaluation.](#11-enable-traceability-make-it-easy-to-understand-flag-evaluation) - -## Background - -Feature flags have become a central part of the DevOps toolbox along with Git, CI/CD and microservices. You can write modern software without all of these things, but it sure is a lot harder, and a lot less fun. - -And just like the wrong Git repo design can cause interminable headaches, getting the details wrong when first building a feature flag system can be very costly. - -This set of principles for building a large-scale feature management platform is the result of thousands of hours of work building and scaling Unleash, an open-source feature management solution used by thousands of organizations. +At Unleash, we've defined 11 principles for building a large-scale feature flag system. These principles have their roots in distributed systems design and focus on security, privacy, and scalability—critical needs for enterprise systems. By following these principles, you can create a feature flag system that's reliable, easy to maintain, and capable of handling heavy loads. -Before Unleash was a community and a company, it was an internal project, started by [one dev](https://github.com/ivarconr), for one company. As the community behind Unleash grew, patterns and anti-patterns of large-scale feature flag systems emerged. Our community quickly discovered that these are important principles for anyone who wanted to avoid spending weekends debugging the production system that is supposed to make debugging in production easier. - -“Large scale” means the ability to support millions of flags served to end-users with minimal latency or impact on application uptime or performance. That is the type of system most large enterprises are building today and the type of feature flag system that this guide focuses on. - -Our motivation for writing these principles is to share what we’ve learned building a large-scale feature flag solution with other architects and engineers solving similar challenges. Unleash is open-source, and so are these principles. Have something to contribute? [Open a PR](https://github.com/Unleash/unleash/pulls) or [discussion](https://github.com/orgs/Unleash/discussions) on our Github. - -## 1. Enable run-time control. Control flags dynamically, not using config files. - -A scalable feature management system evaluates flags at runtime. Flags are dynamic, not static. If you need to restart your application to turn on a flag, you are using configuration, not feature flags. +These principles are: -A large-scale feature flag system that enables runtime control should have at minimum the following components: +1. [Enable runtime control](#1-enable-runtime-control) +2. [Protect PII by evaluating flags server-side](#2-protect-pii-by-evaluating-flags-server-side) +3. [Evaluate flags as close to the user as possible](#3-evaluate-flags-as-close-to-the-user-as-possible) +4. [Scale horizontally by decoupling reads and writes](#4-scale-horizontally-by-decoupling-reads-and-writes) +5. [Limit feature flag payload](#5-limit-feature-flag-payload) +6. [Prioritize availability over consistency](#6-prioritize-availability-over-consistency) +7. [Make flags short-lived](#7-make-flags-short-lived) +8. [Ensure unique flag names](#8-ensure-unique-flag-names) +9. [Choose open by default](#9-choose-open-by-default) +10. [Prioritize consistent user experience](#10-prioritize-consistent-user-experience) +11. [Optimize for developer experience](#11-optimize-for-developer-experience) -**1. Feature Flag Control Service**: Use a centralized feature flag service that acts as the control plane for your feature flags. This service will handle flag configuration. The scope of this service should reflect the boundaries of your organization. +Let's dive deeper into each principle. -Independent business units or product lines should potentially have their own instances, while business units or product lines that work closely together should most likely use the same instance in order to facilitate collaboration. This will always be a contextual decision based on your organization and how you organize the work, but keep in mind that you’d like to keep the management of the flags as simple as possible to avoid the complexity of cross-instance synchronization of feature flag configuration. +## 1. Enable runtime control -**2. Database or Data Store**: Use a robust and scalable database or data store to store feature flag configurations. Popular choices include SQL or NoSQL databases or key-value stores. Ensure that this store is highly available and reliable. +A scalable feature management system evaluates flags at runtime. Flags are dynamic, not static. If you need to restart your application to turn on a flag, that's configuration, not a feature flag. -**3. API Layer**: Develop an API layer that exposes endpoints for your application to interact with the Feature Flag Control Service. This API should allow your application to request feature flag configurations. +A large-scale feature flag system that enables runtime control should have, at minimum, the following components: a service to manage feature flags, a database or data store, an API layer, a feature flag SDK, and a continuous update mechanism. -**4. Feature Flag SDK**: Build or integrate a feature flag SDK into your application. This SDK should provide an easy-to-use interface for fetching flag configurations and evaluating feature flags at runtime. When evaluating feature flags in your application, the call to the SDK should query the local cache, and the SDK should ask the central service for updates in the background. +Let's break down these components. -Build SDK bindings for each relevant language in your organization. Make sure that the SDKs uphold a standard contract governed by a set of feature flag client specifications that documents what functionality each SDK should support. +- **Feature Flag Control Service**: A service that acts as the control plane for your feature flags, managing all flag configurations. The scope of this service should reflect the boundaries of your organization. +- **Database or data store**: A robust, scalable, and highly available database or data store that stores feature flag configurations reliably. Common options include SQL databases, NoSQL databases, or key-value stores. +- **API layer**: An API layer that exposes endpoints for your application to interact with the _Feature Flag Control Service_. This API should allow your application to request feature flag configurations. +- **Feature flag SDK**: An easy-to-use interface for fetching flag configurations and evaluating feature flags at runtime. When considering feature flags in your application, the call to the SDK should query the local cache, and the SDK should ask the central service for updates in the background. +- **Continuous update mechanism**: An update mechanism that enables dynamic updates to feature flag configurations without requiring application restarts or redeployments. The SDK should handle subscriptions or polling to the _Feature Flag Control Service_ for updates. -
+
-**5. Continuously Updated**: Implement update mechanisms in your application so that changes to feature flag configurations are reflected without requiring application restarts or redeployments. The SDK should handle subscriptions or polling to the feature flag service for updates. +## 2. Protect PII by evaluating flags server-side -## 2. Never expose PII. Follow the principle of least privilege. +Feature flags often require contextual data for accurate evaluation, which could include sensitive information such as user IDs, email addresses, or geographical locations. To safeguard this data, follow the data security [principle of least privilege (PoLP)](https://www.cyberark.com/what-is/least-privilege), ensuring that all [Personally Identifiable Information (PII)](https://www.investopedia.com/terms/p/personally-identifiable-information-pii.asp) remains confined to your application. -To keep things simple, you may be tempted to evaluate the feature flags in your Feature Flag Control Service. Don’t. Your Feature Flag Control Service should only handle the configuration for your feature flags and pass this configuration down to SDKs connecting from your applications. +To implement the principle of least privilege, ensure that your _Feature Flag Control Service_ only handles the configuration for your feature flags and passes this configuration down to the SDKs connecting from your applications. -The primary rationale behind this practice is that feature flags often require contextual data for accurate evaluation. This may include user IDs, email addresses, or geographical locations that influence whether a flag should be toggled on or off. Safeguarding this sensitive information from external exposure is paramount. This information may include Personally Identifiable Information (PII), which must remain confined within the boundaries of your application, following the data security principle of least privilege (PoLP). +Let's look at an example where feature flag evaluation happens inside the server-side application. This is where all the contextual application data lives. The flag configuration—all the information needed to evaluate the flags—is fetched from the _Feature Flag Control Service_. -
+
-For client-side applications where the code resides on the user's machine, such as in the browser or on mobile devices, you’ll want to take a different approach. You can’t evaluate on the client side because it raises significant security concerns by exposing potentially sensitive information such as API keys, flag data, and flag configurations. Placing these critical elements on the client side increases the risk of unauthorized access, tampering, or data breaches. +Client-side applications where the code resides on the user's machine in browsers or mobile devices, require a different approach. You can't evaluate flags on the client side because it raises significant security concerns by exposing potentially sensitive information such as API keys, flag data, and flag configurations. Placing these critical elements on the client side increases the risk of unauthorized access, tampering, or data breaches. -Instead of performing client-side evaluation, a more secure and maintainable approach is to conduct feature flag evaluation within a self-hosted environment. Doing so can safeguard sensitive elements like API keys and flag configurations from potential client-side exposure. This strategy involves a server-side evaluation of feature flags, where the server makes decisions based on user and application parameters and then securely passes down the evaluated results to the frontend without any configuration leaking. +Instead of performing client-side evaluation, a more secure and maintainable approach is to evaluate feature flags within a self-hosted environment. Doing so can safeguard sensitive elements like API keys and flag configurations from potential client-side exposure. This strategy involves a server-side evaluation of feature flags, where the server makes decisions based on user and application parameters and then securely passes down the evaluated results to the frontend without any configuration leaking.
-Here’s how you can architect your solution to minimize PII or configuration leakage: - -1. **Server-Side Components**: - -In Principle 1, we proposed a set of architectural principles and components to set up a Feature Flag Control Service. The same architecture patterns apply here, with additional suggestions for achieving local evaluation. Refer to Principle 1 for patterns to set up a feature flagging service. - -**Feature Flag Evaluation Service**: If you need to use feature flags on the client side, where code is delivered to users' devices, you’ll need an evaluation server that can evaluate feature flags and pass evaluated results down to the SDK in the client application. - -2. **SDKs**: - -SDKs will make it more comfortable to work with feature flags. Depending on the context of your infrastructure, you need different types of SDKs to talk to your feature flagging service. For the server side, you’ll need SDKs that can talk directly to the feature flagging service and fetch the configuration. - -The server-side SDKs should implement logic to evaluate feature flags based on the configuration received from the Feature Flag Control Service and the application-specific context. Local evaluation ensures that decisions are made quickly without relying on network roundtrips. - -For client-side feature flags, you’ll need a different type of SDK. These SDKs will send the context to the Feature Flag Evaluation Service and receive the evaluated results. These results should be stored in memory and used when doing a feature flag lookup in the client-side application. By keeping the evaluated results for a specific context in memory in the client-side application, you avoid network roundtrips every time your application needs to check the status of a feature flag. It achieves the same level of performance as a server-side SDK, but the content stored in memory is different and limited to evaluated results on the client. - -The benefits of this approach include: - -**Privacy Protection**: - -a. **Data Minimization**: By evaluating feature flags in this way, you minimize the amount of data that needs to be sent to the Feature Flag Control Service. This can be crucial for protecting user privacy, as less user-specific data is transmitted over the network. - -b. **Reduced Data Exposure**: Sensitive information about your users or application's behavior is less likely to be exposed to potential security threats. Data breaches or leaks can be mitigated by limiting the transmission of sensitive data. - -## 3. Evaluate flags as close to the user as possible. Reduce latency. - -Feature flags should be evaluated as close to your users as possible, and the evaluation should always happen server side as discussed in Principle 2. In addition to security and privacy benefits, performing evaluation as close as possible to your users has multiple benefits: - -1. **Performance Efficiency**: - - a. **Reduced Latency**: Network roundtrips introduce latency, which can slow down your application's response time. Local evaluation eliminates the need for these roundtrips, resulting in faster feature flag decisions. Users will experience a more responsive application, which can be critical for maintaining a positive user experience. - - b. **Offline Functionality**: Applications often need to function offline or in low-connectivity environments. Local evaluation ensures that feature flags can still be used and decisions can be made without relying on a network connection. This is especially important for mobile apps or services in remote locations. - -2. **Cost Savings**: - - a. **Reduced Bandwidth Costs**: Local evaluation reduces the amount of data transferred between your application and the feature flag service. This can lead to significant cost savings, particularly if you have a large user base or high traffic volume. - -3. **Offline Development and Testing**: - - a. **Development and Testing**: Local evaluation is crucial for local development and testing environments where a network connection to the feature flag service might not be readily available. Developers can work on feature flag-related code without needing constant access to the service, streamlining the development process. - -4. **Resilience**: - - a. **Service Outages**: If the feature flag service experiences downtime or outages, local evaluation allows your application to continue functioning without interruptions. This is important for maintaining service reliability and ensuring your application remains available even when the service is down. - -In summary, this principle emphasizes the importance of optimizing performance while protecting end-user privacy by evaluating feature flags as close to the end user as possible. Done right, this also leads to a highly available feature flag system that scales with your applications. - -## 4. Scale Horizontally. Decouple reading and writing flags. - -Separating the reading and writing of feature flags into distinct APIs is a critical architectural decision for building a scalable and efficient feature flag system, particularly when considering horizontal scaling. This separation provides several benefits: - -
- -1. **Horizontal Scaling**: - - - By separating read and write APIs, you can horizontally scale each component independently. This enables you to add more servers or containers to handle increased traffic for reading feature flags, writing updates, or both, depending on the demand. - -2. **Caching Efficiency**: - - - Feature flag systems often rely on caching to improve response times for flag evaluations. Separating read and write APIs allows you to optimize caching strategies independently. For example, you can cache read operations more aggressively to minimize latency during flag evaluations while still ensuring that write operations maintain consistency across the system. - -3. **Granular Access Control**: - - - Separation of read and write APIs simplifies access control and permissions management. You can apply different security measures and access controls to the two APIs. This helps ensure that only authorized users or systems can modify feature flags, reducing the risk of accidental or unauthorized changes. - -4. **Better Monitoring and Troubleshooting**: - - - Monitoring and troubleshooting become more straightforward when read and write operations are separated. It's easier to track and analyze the performance of each API independently. When issues arise, you can isolate the source of the problem more quickly and apply targeted fixes or optimizations. - -5. **Flexibility and Maintenance**: +Here's how you can architect your solution to protect PII and flag configuration data: - - Separation of concerns makes your system more flexible and maintainable. Changes or updates to one API won't directly impact the other, reducing the risk of unintended consequences. This separation allows development teams to work on each API separately, facilitating parallel development and deployment cycles. +### Server-side components -6. **Load Balancing**: +In [Principle 1](#1-enable-runtime-control), we proposed a set of architectural components for building a feature flag system. The same principles apply here, with additional suggestions for achieving local evaluation. For client-side setups, use a dedicated evaluation server that can evaluate feature flags and pass evaluated results to the client SDK. - - Load balancing strategies can be tailored to the specific needs of the read and write APIs. You can distribute traffic and resources accordingly to optimize performance and ensure that neither API becomes a bottleneck under heavy loads. +### SDKs -## 5. Limit payloads. Feature flag payload should be as small as possible. +[SDKs](/reference/sdks) make it more convenient to work with feature flags. Depending on the context of your infrastructure, you need different types of SDKs to talk to your feature flagging service. Server-side SDKs should fetch configurations from the _Feature Flag Control Service_ and evaluate flags locally using the application's context, reducing the need for frequent network calls. -Minimizing the size of feature flag payloads is a critical aspect of maintaining the efficiency and performance of a feature flag system. The configuration of your feature flags can vary in size depending on the complexity of your targeting rules. For instance, if you have a targeting engine that determines whether a feature flag should be active or inactive based on individual user IDs, you might be tempted to include all these user IDs within the configuration payload. While this approach may work fine for a small user base, it can become unwieldy when dealing with a large number of users. +For client-side feature flags, SDKs should send the context to an evaluation server and receive the evaluated results. The evaluated results are then cached in memory in the client-side application, allowing quick lookups without additional network overhead. This provides the performance benefits of local evaluation while minimizing the exposure of sensitive data. -If you find yourself facing this challenge, your instinct might be to store this extensive user information directly in the feature flagging system. However, this can also run into scaling problems. A more efficient approach is to categorize these users into logical groupings at a different layer and then use these group identifiers when you evaluate flags within your feature flagging system. For example, you can group users based on their subscription plan or geographical location. Find a suitable parameter for grouping users, and employ those group parameters as targeting rules in your feature flagging solution. +This approach enhances privacy by minimizing the amount of sensitive data sent to the _Feature Flag Control Service_, reducing the risk of data exposure and potential security threats. -Imposing limitations on payloads is crucial for scaling a feature flag system: +## 3. Evaluate flags as close to the user as possible -1. **Reduced Network Load**: +For optimal performance, you should evaluate feature flags as close to your users as possible. Building on the server-side evaluation approach from [Principle 2](#2-protect-pii-by-evaluating-flags-server-side), let's look at how evaluating flags locally can bring key benefits in terms of performance, cost, and reliability: - - Large payloads, especially for feature flag evaluations, can lead to increased network traffic between the application and the feature flagging service. This can overwhelm the network and cause bottlenecks, leading to slow response times and degraded system performance. Limiting payloads helps reduce the amount of data transferred over the network, alleviating this burden. Even small numbers become large when multiplied by millions. +- **Reduced latency**: Network roundtrips introduce latency, slowing your application's response time. Local evaluation eliminates the need for these roundtrips, resulting in faster feature flag decisions. This makes your application more responsive thereby improving the user experience. +- **Offline functionality**: Many applications need to function offline or in low-connectivity environments. Local evaluation ensures feature flags are still functional, even without an active network connection. This is especially important for mobile apps or services in remote locations. +- **Reduced bandwidth costs**: Local evaluation reduces the amount of data transferred between your application and the feature flag service. This can lead to significant cost savings, particularly if you have a large user base or high traffic volume. +- **Ease of development and testing**: Developers can continue their work in environments where a network connection to the feature flag service might be unstable or unavailable. Local evaluation allows teams to work on feature flag-related code without needing constant access to the service, streamlining the development process. +- **Resilience during service downtime**: If the feature flag service experiences downtime or outages, local evaluation allows your application to continue functioning without interruptions. This is important for maintaining service reliability and ensuring your application remains available even when the service is down. -2. **Faster Evaluation**: +In summary, this principle emphasizes optimizing performance while protecting end-user privacy by evaluating feature flags as close to the end user as possible. This also leads to a highly available feature flag system that scales with your applications. - - Smaller payloads reduce latency which means quicker transmission and evaluation. Speed is essential when evaluating feature flags, especially for real-time decisions that impact user experiences. Limiting payloads ensures evaluations occur faster, allowing your application to respond promptly to feature flag changes. +## 4. Scale horizontally by decoupling reads and writes -3. **Improved Memory Efficiency**: +When designing a scalable feature flag system, one of the most effective strategies is to separate read and write operations into distinct APIs. This architectural decision not only allows you to scale each component independently but also provides better performance, reliability, and control. - - Feature flagging systems often store flag configurations in memory for quick access during runtime. Larger payloads consume more memory, potentially causing memory exhaustion and system crashes. By limiting payloads, you ensure that the system remains memory-efficient, reducing the risk of resource-related issues. +
-4. **Scalability**: +By decoupling read and write operations, you gain the flexibility to scale horizontally based on the unique demands of your application. For example, if read traffic increases, you can add more servers or containers to handle the load without needing to scale the write operations. - - Scalability is a critical concern for modern applications, especially those experiencing rapid growth. Feature flagging solutions need to scale horizontally to accommodate increased workloads. Smaller payloads require fewer resources for processing, making it easier to scale your system horizontally. +The benefits of decoupling read and write operations extend beyond just scalability; let's look at a few others: +- **More efficient caching**: You can optimize your flag caching for read operations to reduce latency while keeping write operations consistent. +- **Granular access control**: You can apply different security measures and access controls to the two APIs, reducing the risk of accidental or unauthorized changes. +- **Improved monitoring and troubleshooting**: Monitoring and troubleshooting become more straightforward. It's easier to track and analyze the performance of each API independently. When issues arise, you can isolate the source of the problem more quickly and apply targeted fixes or optimizations. +- **Flexibility and maintenance**: Updates to one API won't directly impact the other, reducing the risk of unintended consequences. This separation of concerns allows development teams to work on each API separately, facilitating parallel development and deployment cycles. +- **Distributed traffic**: You can tailor load-balancing strategies to the specific needs of the read and write APIs. You can distribute traffic and resources accordingly to optimize performance and ensure that neither API becomes a bottleneck under heavy load. -5. **Lower Infrastructure Costs**: +## 5. Limit feature flag payload - - When payloads are limited, the infrastructure required to support the feature flagging system can be smaller and less costly. This saves on infrastructure expenses and simplifies the management and maintenance of the system. +Minimizing the size of feature flag payloads is a critical aspect of maintaining the efficiency and performance of a feature flag system. Payload size can vary based on targeting rule complexity. For example, targeting based on individual user IDs may work with small user bases but becomes inefficient as the user base grows. -6. **Reliability**: +Avoid storing large user lists directly in the feature flag configuration, which can lead to scaling issues. Instead, categorize users into logical groups at a higher layer (for example, by subscription plan or location) and use group identifiers for targeting within the feature flag system. - - A feature flagging system that consistently delivers small, manageable payloads is more likely to be reliable. It reduces the risk of network failures, timeouts, and other issues when handling large data transfers. Reliability is paramount for mission-critical applications. +Keeping the feature flag payload small results in: -7. **Ease of Monitoring and Debugging**: +- **Reduced network load**: Large payloads can lead to increased network traffic between the application and the feature flagging service. This can overwhelm the network and cause bottlenecks, leading to slow response times and degraded system performance. Even small optimizations can make a big difference at scale. +- **Faster flag evaluation**: Smaller payloads mean faster data transmission and flag evaluation, crucial for real-time decisions that affect user experience. +- **Improved memory efficiency**: Feature flagging systems often store flag configurations in memory for quick access during runtime. Larger payloads consume more memory, potentially causing memory exhaustion and system crashes. Limiting payloads ensures that the system remains memory-efficient, reducing the risk of resource-related issues. +- **Better scalability**: Smaller payloads require fewer resources, making it easier to scale your system as your application grows. +- **Lower infrastructure costs**: Optimized payloads reduce infrastructure needs and costs while simplifying system management. +- **Improved system reliability**: Delivering smaller, more manageable payloads minimizes the risk of network timeouts and failures. +- **Ease of monitoring and debugging**: Smaller payloads are easier to monitor and debug, making issue resolution faster. - - Smaller payloads are easier to monitor and debug. When issues arise, it's simpler to trace problems and identify their root causes when dealing with smaller, more manageable data sets. +For more insights into reducing payload size, visit our [Best practices for using feature flags at scale](/topics/feature-flags/best-practices-using-feature-flags-at-scale#14-avoid-giant-feature-flag-targeting-lists) guide. -## 6. Design for failure. Favor availability over consistency. +## 6. Prioritize availability over consistency -Your feature flag system should not be able to take down your main application under any circumstance, including network disruptions. Follow these patterns to achieve fault tolerance for your feature flag system. +Your application shouldn't have any dependency on the availability of your feature flag system. Robust feature flag systems avoid relying on real-time flag evaluations because the unavailability of the feature flag system will cause application downtime, outages, degraded performance, or even a complete failure of your application. -**Zero dependencies**: Your application's availability should have zero dependencies on the availability of your feature flag system. Robust feature flag systems avoid relying on real-time flag evaluations because the unavailability of the feature flag system will cause application downtime, outages, degraded performance, or even a complete failure of your application. +If the feature flag system fails, your application should continue running smoothly. Feature flagging should degrade gracefully, preventing any unexpected behavior or disruptions for users. -**Graceful degradation**: If the system goes down, it should not disrupt the user experience or cause unexpected behavior. Feature flagging should gracefully degrade in the absence of the Feature Flag Control service, ensuring that users can continue to use the application without disruption. +You can implement the following strategies to achieve a resilient architecture: -**Resilient Architecture Patterns**: +- **Bootstrap SDKs with data**: Feature flagging SDKs should work with locally cached data, even when the network connection to the _Feature Flag Control Service_ is unavailable, using the last known configuration or defaults to ensure uninterrupted functionality. +- **Use local cache**: Maintaining a local cache of feature flag configurations helps reduce network round trips and dependency on external services. The local cache can periodically synchronize with the central _Feature Flag Control Service_ when it's available. This approach minimizes the impact of network failures or service downtime on your application. +- **Evaluate feature flags locally**: Whenever possible, the SDKs or application components should evaluate feature flags locally without relying on external services, ensuring uninterrupted feature flag evaluations even if the feature flagging service is down. +- **Prioritize availability over consistency**: In line with the [CAP theorem](https://www.ibm.com/topics/cap-theorem), design for availability over strict consistency. In the face of network partitions or downtime of external services, your application should favor maintaining its availability rather than enforcing perfectly consistent feature flag configuration caches. Eventually consistent systems can tolerate temporary inconsistencies in flag evaluations without compromising availability. -- **Bootstrapping SDKs with Data**: Feature flagging SDKs used within your application should be designed to work with locally cached data, even when the network connection to the Feature Flag Control service is unavailable. The SDKs can bootstrap with the last known feature flag configuration or default values to ensure uninterrupted functionality. +## 7. Make flags short-lived -- **Local Cache**: Maintaining a local cache of feature flag configuration helps reduce network round trips and dependency on external services. The local cache can be periodically synchronized with the central Feature Flag Control service when it's available. This approach minimizes the impact of network failures or service downtime on your application. +The most common use case for feature flags is to manage the rollout of new functionality. Once a rollout is complete, you should remove the feature flag from your code and archive it. Remove any old code paths that the new functionality replaces. -- **Evaluate Locally**: Whenever possible, the SDKs or application components should be able to evaluate feature flags locally without relying on external services. This ensures that feature flag evaluations continue even when the feature flagging service is temporarily unavailable. +Avoid using feature flags for static application configuration. Application configuration should be consistent, long-lived, and loaded during application startup. In contrast, feature flags are intended to be short-lived, dynamic, and updated at runtime. They prioritize availability over consistency and are designed to be modified frequently. -- **Availability Over Consistency**: As the CAP theorem teaches us, in distributed systems, prioritizing availability over strict consistency can be a crucial design choice. This means that, in the face of network partitions or downtime of external services, your application should favor maintaining its availability rather than enforcing perfectly consistent feature flag configuration caches. Eventually consistent systems can tolerate temporary inconsistencies in flag evaluations without compromising availability. In CAP theorem parlance, a feature flagging system should aim for AP over CP. +To succeed with feature flags in a large organization, follow these strategies: -By implementing these resilient architecture patterns, your feature flagging system can continue to function effectively even in the presence of downtime or network disruptions in the feature flagging service. This ensures that your main application remains stable, available, and resilient to potential issues in the feature flagging infrastructure, ultimately leading to a better user experience and improved reliability. +- **Set flag expiration dates**: Assign expiration dates to feature flags to track which flags are no longer needed. A good feature flag management tool will alert you to expired flags, making it easier to maintain your codebase. +- **Treat feature flags like technical debt**: Incorporate tasks to remove outdated feature flags into your sprint or project planning, just as you would with technical debt. Feature flags add complexity to your code by introducing multiple code paths that need context and maintenance. If you don't clean up feature flags in a timely manner, you risk losing the context as time passes or personnel changes, making them harder to manage or remove. +- **Archive old flags**: When feature flags are no longer in use, archive them after removing them from the codebase. This archive serves as an important audit log of feature flags and allows you to revive flags if you need to restore an older version of your application. -## 7. Make feature flags short-lived. Do not confuse flags with application configuration. +While most feature flags should be short-lived, there are valid exceptions for long-lived flags, including: +- **Kill switches**: These act as inverted feature flags, allowing you to gracefully disable parts of a system with known weak spots. +- **Internal flags**: Used to enable additional debugging, tracing, and metrics at runtime, which are too costly to run continuously. Engineers can enable these flags while debugging issues. -Feature flags have a lifecycle shorter than an application lifecycle. The most common use case for feature flags is to protect new functionality. That means that when the roll-out of new functionality is complete, the feature flag should be removed from the code and archived. If there were old code paths that the new functionality replaces, those should also be cleaned up and removed. +## 8. Ensure unique flag names -Feature flags should not be used for static application configuration. Application configuration is expected to be consistent, long-lived, and read when launching an application. Using feature flags to configure an application can lead to inconsistencies between different instances of the same application. Feature flags, on the other hand, are designed to be short-lived, dynamic, and changed at runtime. They are expected to be read and updated at runtime and favor availability over consistency. - -To succeed with feature flags in a large organization, you should: - -- **Use flag expiration dates**: By setting expiration dates for your feature flags, you make it easier to keep track of old feature flags that are no longer needed. A proper feature flag solution will inform you about potentially expired flags. - -- **Treat feature flags like technical debt.**: You must plan to clean up old feature branches in sprint or project planning, the same way you plan to clean up technical debt in your code. Feature flags add complexity to your code. You’ll need to know what code paths the feature flag enables, and while the feature flag lives, the context of it needs to be maintained and known within the organization. If you don’t clean up feature flags, eventually, you may lose the context surrounding it if enough time passes and/or personnel changes happen. As time passes, you will find it hard to remove flags, or to operate them effectively. - -- **Archive old flags**: Feature flags that are no longer in use should be archived after their usage has been removed from the codebase. The archive serves as an important audit log of feature flags that are no longer in use, and allows you to revive them if you need to install an older version of your application. - -There are valid exceptions to short-lived feature flags. In general, you should try to limit the amount of long-lived feature flags. Some examples include: - -- Kill-switches - these work like an inverted feature flag and are used to gracefully disable part of a system with known weak spots. -- Internal flags used to enable additional debugging, tracing, and metrics at runtime, which are too costly to run all the time. These can be enabled by software engineers while debugging issues. - -## 8. Use unique names across all applications. Enforce naming conventions. - -All flags served by the same Feature Flag Control service should have unique names across the entire cluster to avoid inconsistencies and errors. - -- **Avoid zombies:** Uniqueness should be controlled using a global list of feature flag names. This prevents the reuse of old flag names to protect new features. Using old names can lead to accidental exposure of old features, still protected with the same feature flag name. -- **Naming convention enforcement: **Ideally, unique names are enforced at creation time. In a large organization, it is impossible for all developers to know all flags used. Enforcing a naming convention makes naming easier, ensures consistency, and provides an easy way to check for uniqueness. +Ensure that all flags within the same _Feature Flag Control Service_ have unique names across your entire system. Unique naming prevents the reuse of old flag names, reducing the risk of accidentally re-enabling outdated features with the same name. Unique naming has the following advantages: +- **Flexibility over time**: Large enterprise systems are not static. Monoliths may split into microservices, microservices may merge, and applications change responsibility. Unique flag naming across your organization means that you can reorganize your flags to match the changing needs of your organization. +- **Fewer conflicts**: If two applications use the same feature flag name, it can become difficult to identify which flag controls which application. Even with separate namespaces, you risk toggling the wrong flag, leading to unexpected consequences. +- **Easier flag management**: Unique names make it simpler to track and identify feature flags. Searching across codebases becomes more straightforward, and it's easier to understand a flag's purpose and where it's used. +- **Improved collaboration**: A feature flag with a unique name in the organization simplifies collaboration across teams, products, and applications, ensuring that everyone refers to the same feature. -- **Flexibility over time: **Large enterprise systems are not static. Over time, monoliths are split into microservices, microservices are merged into larger microservices, and applications change responsibility. This means that the way flags are grouped will change over time, and a unique name for the entire organization ensures that you keep the option to reorganize your flags to match the changing needs of your organization. -- **Prevent conflicts**: If two applications use the same Feature Flag name it can be impossible to know which flag is controlling which applications. This can lead to accidentally flipping the wrong flag, even if they are separated into different namespaces (projects, workspaces etc). -- **Easier to manage: **It's easier to know what a flag is used for and where it is being used when it has a unique name. E.g. It will be easier to search across multiple code bases to find references for a feature flag when it has a unique identifier across the entire organization. -- **Enables collaboration:** When a feature flag has a unique name in the organization, it simplifies collaboration across teams, products and applications. It ensures that we all talk about the same feature. - -## 9. Choose open by default. Democratize feature flag access. - -Allowing engineers, product owners, and even technical support to have open access to a feature flagging system is essential for effective development, debugging, and decision-making. These groups should have access to the system, along with access to the codebase and visibility into configuration changes: - -1. **Debugging and Issue Resolution**: - - - **Code Access**: Engineers should have access to the codebase where feature flags are implemented. This access enables them to quickly diagnose and fix issues related to feature flags when they arise. Without code access, debugging becomes cumbersome, and troubleshooting becomes slower, potentially leading to extended downtimes or performance problems. - -2. **Visibility into Configuration**: - - - **Configuration Transparency**: Engineers, product owners, and even technical support should be able to view the feature flag configuration. This transparency provides insights into which features are currently active, what conditions trigger them, and how they impact the application's behavior. It helps understand the system's state and behavior, which is crucial for making informed decisions. - - - **Change History**: Access to a history of changes made to feature flags, including who made the changes and when, is invaluable. This audit trail allows teams to track changes to the system's behavior over time. It aids in accountability and can be instrumental in troubleshooting when unexpected behavior arises after a change. - - - **Correlating Changes with Metrics**: Engineers and product owners often need to correlate feature flag changes with production application metrics. This correlation helps them understand how feature flags affect user behavior, performance, and system health. It's essential for making data-driven decisions about feature rollouts, optimizations, or rollbacks. - -3. **Collaboration**: - - - **Efficient Communication**: Open access fosters efficient communication between engineers and the rest of the organization. When it's open by default, everyone can see the feature flagging system and its changes, and have more productive discussions about feature releases, experiments, and their impact on the user experience. - -4. **Empowering Product Decisions**: - - - **Product Owner Involvement**: Product owners play a critical role in defining feature flags' behavior and rollout strategies based on user needs and business goals. Allowing them to access the feature flagging system empowers them to make real-time decisions about feature releases, rollbacks, or adjustments without depending solely on engineering resources. - -5. **Security and Compliance**: - - - **Security Audits**: Users of a feature flag system should be part of corporate access control groups such as SSO. Sometimes, additional controls are necessary, such as feature flag approvals using the four-eyes principle. - -Access control and visibility into feature flag changes are essential for security and compliance purposes. It helps track and audit who has made changes to the system, which can be crucial in maintaining data integrity and adhering to regulatory requirements. - -## 10. Do no harm. Prioritize consistent user experience. - -Feature flagging solutions are indispensable tools in modern software development, enabling teams to manage feature releases and experiment with new functionality. However, one aspect that is absolutely non-negotiable in any feature flag solution is the need to ensure a consistent user experience. This isn't a luxury; it's a fundamental requirement. Feature flagging solutions must prioritize consistency and guarantee the same user experience every time, especially with percentage-based gradual rollouts. - -**Why Consistency is Paramount:** - -1. **User Trust**: Consistency breeds trust. When users interact with an application, they form expectations about how it behaves. Any sudden deviations can erode trust and lead to a sense of unreliability. - -2. **Reduced Friction**: Consistency reduces friction. Users shouldn't have to relearn how to use an app every time they open it. A consistent experience reduces the cognitive load on users, enabling them to engage effortlessly. - -3. **Quality Assurance**: Maintaining a consistent experience makes quality assurance more manageable. It's easier to test and monitor when you have a reliable benchmark for the user experience. - -4. **Support and Feedback**: Inconsistent experiences lead to confused users, increased support requests, and muddied user feedback. Consistency ensures that user issues are easier to identify and address. - -5. **Brand Integrity**: A consistent experience reflects positively on your brand. It demonstrates professionalism and commitment to user satisfaction, enhancing your brand's reputation. - -**Strategies for Consistency in Percentage-Based Gradual Rollouts:** - -1. **User Hashing**: Assign users to consistent groups using a secure hashing algorithm based on unique identifiers like user IDs or emails. This ensures that the same user consistently falls into the same group. - -2. **Segmentation Control**: Provide controls within the feature flagging tool to allow developers to segment users logically. For instance, segment by location, subscription type, or any relevant criteria to ensure similar user experiences. - -3. **Fallback Mechanisms**: Include fallback mechanisms in your architecture. If a user encounters issues or inconsistencies, the system can automatically switch them to a stable version or feature state. - -4. **Logging and Monitoring**: Implement robust logging and monitoring. Continuously track which users are in which groups and what version of the feature they are experiencing. Monitor for anomalies or deviations and consider building automated processes to disable features that may be misbehaving. - -5. **Transparent Communication**: Clearly communicate the gradual rollout to users. Use in-app notifications, tooltips, or changelogs to inform users about changes, ensuring they know what to expect. - -In summary, consistency is a cornerstone of effective feature flagging solutions. When designing an architecture for percentage-based gradual rollouts, prioritize mechanisms that guarantee the same user gets the same experience every time. This isn't just about good software practice; it's about respecting your users and upholding their trust in your application. By implementing these strategies, you can create a feature flagging solution that empowers your development process and delights your users with a dependable and consistent experience. - -## 11. Enable traceability. Make it easy to understand flag evaluation. - -Developer experience is a critical factor to consider when implementing a feature flag solution. A positive developer experience enhances the efficiency of the development process and contributes to the overall success and effectiveness of feature flagging. One crucial aspect of developer experience is ensuring the testability of the SDK and providing tools for developers to understand how and why feature flags are evaluated. This is important because: - -1. **Ease of Testing and Debugging:** - - - **Faster Development Cycles:** A feature flagging solution with a testable SDK allows developers to quickly test and iterate on new features. They can easily turn flags on or off, simulate different conditions, and observe the results without needing extensive code changes or redeployments. - - - **Rapid Issue Resolution:** When issues or unexpected behavior arise, a testable SDK enables developers to pinpoint the problem more efficiently. They can examine the flag configurations, log feature flag decisions, and troubleshoot issues more precisely. - -2. **Visibility into Flag Behaviour:** - - - **Understanding User Experience:** Developers need tools to see and understand how feature flags affect the user experience. This visibility helps them gauge the impact of flag changes and make informed decisions about when to roll out features to different user segments. Debugging a feature flag with multiple inputs simultaneously makes it easy for developers to compare the results and quickly figure out how a feature flag evaluates in different scenarios with multiple input values. +## 9. Choose open by default - - **Enhanced Collaboration:** Feature flagging often involves cross-functional teams, including developers, product managers, and QA testers. Providing tools with a clear view of flag behavior fosters effective collaboration and communication among team members. +At Unleash, we believe in democratizing feature flag access. Making feature flag systems open by default enables engineers, product owners, and support teams to collaborate effectively and make informed decisions. Open access encourages productive discussions about feature releases, experiments, and their impact on the user experience. -3. **Transparency and Confidence:** +Access control and visibility are also key considerations for security and compliance. Tracking and auditing feature flag changes help maintain data integrity and meet regulatory requirements. While open access is key, it's equally important to integrate with corporate access controls, such as SSO, to ensure security. In some cases, additional controls like feature flag approvals using the [four-eyes principle](/reference/change-requests) are necessary for critical changes. - - **Confidence in Flag Decisions:** A transparent feature flagging solution empowers developers to make data-driven decisions. They can see why a particular flag evaluates to a certain value, which is crucial for making informed choices about feature rollouts and experimentation. +For open collaboration, consider providing the following: +- **Access to the codebase**: Engineers need direct access to the codebase where feature flags are implemented. This allows them to quickly diagnose and fix issues, minimizing downtime and performance problems. +- **Access to configuration**: Engineers, product owners, and even technical support should be able to view feature flag configuration. This transparency provides insights into which features are currently active, what conditions trigger them, and how they impact the application's behavior. Product owners can also make real-time decisions on feature rollouts or adjustments without relying solely on engineering resources. +- **Access to analytics**: Both engineers and product owners should be able to correlate feature flag changes with production metrics. This helps assess how flags impact user behavior, performance, and system health, enabling data-driven decisions for feature rollouts, optimizations, or rollbacks. - - **Reduced Risk:** When developers clearly understand of why flags evaluate the way they do, they are less likely to make unintentional mistakes that could lead to unexpected issues in production. +## 10. Prioritize consistent user experience -4. **Effective Monitoring and Metrics:** +Feature flagging solutions are indispensable tools in modern software development, enabling teams to manage feature releases and experiment with new functionality. However, one aspect that is absolutely non-negotiable in any feature flag solution is the need to ensure a consistent user experience. Feature flagging solutions must prioritize consistency and guarantee the same user experience every time, especially with percentage-based gradual rollouts. - - **Tracking Performance:** A testable SDK should provide developers with the ability to monitor the performance of feature flags in real time. This includes tracking metrics related to flag evaluations, user engagement, and the impact of flag changes. +Strategies for consistency in percentage-based gradual rollouts: - - **Data-Driven Decisions:** Developers can use this data to evaluate the success of new features, conduct A/B tests, and make informed decisions about optimizations. +- **User hashing**: Assign users to consistent groups using a secure hashing algorithm based on unique identifiers like user IDs or emails. This ensures that the same user consistently falls into the same group. +- **Segmentation control**: Provide controls within the feature flagging tool to allow developers to [segment](/reference/segments) users logically by criteria like location, subscription type, or other relevant factors, ensuring similar experiences for users within the same segment. +- **Fallback mechanisms**: Include fallback mechanisms in your architecture. If a user encounters issues or inconsistencies, the system should automatically switch them to a stable version or feature state. +- **Logging and monitoring**: Implement robust logging and monitoring. Continuously track which users are in which groups and what version of the feature they are experiencing. Monitor for anomalies or deviations and consider building automated processes to disable features that may be misbehaving. +- **Transparent communication**: Clearly communicate gradual rollouts through in-app notifications, tooltips, or changelogs, so users are informed about changes and know what to expect. - - **Usage metrics:** A feature flag system should provide insight on an aggregated level about the usage of feature flags. This is helpful for developers so that they can easily assess that everything works as expected. +## 11. Optimize for developer experience -5. **Documentation and Training:** +[Developer experience](https://www.opslevel.com/resources/devex-series-part-1-what-is-devex) is a critical factor to consider when implementing a feature flag solution. A positive developer experience enhances the efficiency of the development process and contributes to the overall success and effectiveness of feature flagging. One key aspect of developer experience is ensuring the testability of the SDK and providing tools for developers to understand how and why feature flags are evaluated. - - **Onboarding and Training:** The entire feature flag solution, including API, UI, and the SDKs, requires clear and comprehensive documentation, along with easy-to-understand examples, in order to simplify the onboarding process for new developers. It also supports the ongoing training of new team members, ensuring that everyone can effectively use the feature flagging solution. +To ensure a good developer experience, you should provide the following: +- **Simplified testing and debugging**: A testable SDK enables developers to quickly test and iterate on features, speeding up development cycles. Developers can toggle flags, simulate conditions, and observe results without significant code changes or redeployments. This makes it easier to identify and fix issues by examining flag configurations and logging decisions. +- **Visibility into flag behavior**: Developers need tools to understand how feature flags impact the user experience. Visibility into flag behavior helps them assess changes, debug effectively with multiple inputs, and collaborate more easily within cross-functional teams. +- **Effective monitoring**: A testable SDK should support real-time monitoring of flag performance, tracking metrics like evaluations, user engagement, and feature impact. Developers can use this data to evaluate the success of new features, conduct A/B tests, and make informed decisions about optimizations. +- **Usage metrics**: Provide aggregated insights into feature flag usage, helping developers confirm that everything is working as expected. +- **Documentation and training**: Offer clear, comprehensive documentation for the API, UI, and SDKs, with easy-to-follow examples. This simplifies onboarding for new developers and supports continuous training, ensuring the effective use of the feature flagging system. -Thank you for reading +Thank you for reading. Our motivation for writing these principles is to share what we've learned building a large-scale feature flag solution with other architects and engineers solving similar challenges. Unleash is open-source, and so are these principles. Have something to contribute? [Open a PR](https://github.com/Unleash/unleash/pulls) or [discussion](https://github.com/orgs/Unleash/discussions) on our GitHub. \ No newline at end of file From 2ec2639fa37520d5a9e4d6cdfba16cfa0565b651 Mon Sep 17 00:00:00 2001 From: Alvin Bryan <107407814+alvinometric@users.noreply.github.com> Date: Tue, 20 Aug 2024 19:00:38 +0100 Subject: [PATCH 02/13] Added video link and fixed types (#7942) Kudos to GitHub for showing the diff despite me renaming the file --- ...-ruby.md => implementing-feature-flags-ruby.mdx} | 7 +++++++ .../rust/implementing-feature-flags-rust.md | 13 +++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) rename website/docs/feature-flag-tutorials/ruby/{implementing-feature-flags-ruby.md => implementing-feature-flags-ruby.mdx} (97%) diff --git a/website/docs/feature-flag-tutorials/ruby/implementing-feature-flags-ruby.md b/website/docs/feature-flag-tutorials/ruby/implementing-feature-flags-ruby.mdx similarity index 97% rename from website/docs/feature-flag-tutorials/ruby/implementing-feature-flags-ruby.md rename to website/docs/feature-flag-tutorials/ruby/implementing-feature-flags-ruby.mdx index f72b44f5b533..0798f45324dd 100644 --- a/website/docs/feature-flag-tutorials/ruby/implementing-feature-flags-ruby.md +++ b/website/docs/feature-flag-tutorials/ruby/implementing-feature-flags-ruby.mdx @@ -4,6 +4,8 @@ description: "How to use Unleash feature flags with Ruby." slug: /feature-flag-tutorials/ruby --- +import VideoContent from '@site/src/components/VideoContent.jsx'; + Hello! In this tutorial we’ll show you how to add feature flags to your Ruby app , using [Unleash](https://www.getunleash.io/) and the official [Unleash Ruby SDK](https://docs.getunleash.io/reference/sdks/ruby). With Unleash, an open-source feature flag service, you can use our tooling to add feature flags to your application and release new features faster. In a classic tutorial fashion, we’ll get a list of planets from the [Star Wars API](https://swapi.dev/), with just Ruby (i.e., not Ruby on Rails). We’ll use feature flags to decide whether to call the REST or the GraphQL version of the API. @@ -17,6 +19,11 @@ In a classic tutorial fashion, we’ll get a list of planets from the [Star Wars - [6. Verify the toggle experience](#6-verify-the-toggle-experience) - [Conclusion](#conclusion) +Watch the video tutorial and follow along with the code from this documentation. + + + + ## Prerequisites For this tutorial, you’ll need the following: diff --git a/website/docs/feature-flag-tutorials/rust/implementing-feature-flags-rust.md b/website/docs/feature-flag-tutorials/rust/implementing-feature-flags-rust.md index 5d1d7ed5f370..477c4d62baea 100644 --- a/website/docs/feature-flag-tutorials/rust/implementing-feature-flags-rust.md +++ b/website/docs/feature-flag-tutorials/rust/implementing-feature-flags-rust.md @@ -204,12 +204,13 @@ We want to let you choose the nature of that concurrency, so we're compatible wi ```rust use enum_map::Enum; use image::ImageReader; +use serde::{Deserialize, Serialize}; use std::error::Error; use std::fs; -use serde::{Deserialize, Serialize}; use std::time::Duration; use tokio::time::sleep; use unleash_api_client::client::ClientBuilder; +use unleash_api_client::Client; use webp::Encoder; #[derive(Debug, Deserialize, Serialize, Enum, Clone)] @@ -218,8 +219,8 @@ enum Flags { } #[tokio::main] -async fn main() -> Result<(), Box> { - let client = ClientBuilder::default().into_client::( +async fn main() -> Result<(), Box> { + let client:Client = ClientBuilder::default().into_client( "http://localhost:4242/api", "unleash-rust-client-example", "unleash-rust-client-example", @@ -232,17 +233,17 @@ async fn main() -> Result<(), Box> { sleep(Duration::from_millis(500)).await; let is_webp = client.is_enabled(Flags::webp, None, false); - process_images(is_webp)?; + process_image(is_webp)?; // allow tokio::join to finish client.stop_poll().await; - Ok::<(), Box>(()) + Ok::<(), Box>(()) }); Ok(()) } -fn process_image(is_webp: bool) -> Result<(), Box> { +fn process_image(is_webp: bool) -> Result<(), Box> { let img = ImageReader::open("input.png")?.decode()?; if is_webp { From 78e9b7bec939c8553c6c91bb2c9414c39f3805dd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 20 Aug 2024 18:06:25 +0000 Subject: [PATCH 03/13] chore(deps): update dependency @types/slug to v5.0.9 (#7944) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [@types/slug](https://togithub.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/slug) ([source](https://togithub.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/slug)) | [`5.0.8` -> `5.0.9`](https://renovatebot.com/diffs/npm/@types%2fslug/5.0.8/5.0.9) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@types%2fslug/5.0.9?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@types%2fslug/5.0.9?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@types%2fslug/5.0.8/5.0.9?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@types%2fslug/5.0.8/5.0.9?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Configuration 📅 **Schedule**: Branch creation - "after 7pm every weekday,before 5am every weekday" in timezone Europe/Madrid, Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **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](https://www.mend.io/free-developer-tools/renovate/). View the [repository job log](https://developer.mend.io/github/Unleash/unleash). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 87f8fbe459a0..d08137ac0036 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2271,9 +2271,9 @@ __metadata: linkType: hard "@types/slug@npm:^5.0.8": - version: 5.0.8 - resolution: "@types/slug@npm:5.0.8" - checksum: 10c0/8e6d308f91e0f099fe6d23f0604fd1576759e40cc1aa0bf8b4eda2c03741c5741840733576a4e7c37b6ae8b5a2adda9458c8a7a16166fbf4167c6108f3abeb15 + version: 5.0.9 + resolution: "@types/slug@npm:5.0.9" + checksum: 10c0/6d5366d80d83a8d08b7d33ea14394511997de2d6001d1e463a5141aa10bf0dd1ec801e0e248b8a84239142064f96918638db7ca2c745c85891d6c34c309ad3a6 languageName: node linkType: hard From 40b06dabb625c3f582df53ab252357bc103fa3a7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 20 Aug 2024 19:10:39 +0000 Subject: [PATCH 04/13] chore(deps): update dependency lint-staged to v15.2.9 (#7945) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [lint-staged](https://togithub.com/lint-staged/lint-staged) | [`15.2.8` -> `15.2.9`](https://renovatebot.com/diffs/npm/lint-staged/15.2.8/15.2.9) | [![age](https://developer.mend.io/api/mc/badges/age/npm/lint-staged/15.2.9?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/lint-staged/15.2.9?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/lint-staged/15.2.8/15.2.9?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/lint-staged/15.2.8/15.2.9?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
lint-staged/lint-staged (lint-staged) ### [`v15.2.9`](https://togithub.com/lint-staged/lint-staged/blob/HEAD/CHANGELOG.md#1529) [Compare Source](https://togithub.com/lint-staged/lint-staged/compare/v15.2.8...v15.2.9) ##### Patch Changes - [#​1463](https://togithub.com/lint-staged/lint-staged/pull/1463) [`b69ce2d`](https://togithub.com/lint-staged/lint-staged/commit/b69ce2ddfd5a7ae576f4fef4afc60b8a81f3c945) Thanks [@​iiroj](https://togithub.com/iiroj)! - Set the maximum number of event listeners to the number of tasks. This should silence the console warning `MaxListenersExceededWarning: Possible EventEmitter memory leak detected`.
--- ### Configuration 📅 **Schedule**: Branch creation - "after 7pm every weekday,before 5am every weekday" in timezone Europe/Madrid, Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **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](https://www.mend.io/free-developer-tools/renovate/). View the [repository job log](https://developer.mend.io/github/Unleash/unleash). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 0e963d689a91..a9adf28bbd5e 100644 --- a/package.json +++ b/package.json @@ -209,7 +209,7 @@ "husky": "^9.0.11", "jest": "29.7.0", "jest-junit": "^16.0.0", - "lint-staged": "15.2.8", + "lint-staged": "15.2.9", "nock": "13.5.4", "openapi-enforcer": "1.23.0", "proxyquire": "2.1.3", diff --git a/yarn.lock b/yarn.lock index d08137ac0036..9dac6aebc97b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6405,9 +6405,9 @@ __metadata: languageName: node linkType: hard -"lint-staged@npm:15.2.8": - version: 15.2.8 - resolution: "lint-staged@npm:15.2.8" +"lint-staged@npm:15.2.9": + version: 15.2.9 + resolution: "lint-staged@npm:15.2.9" dependencies: chalk: "npm:~5.3.0" commander: "npm:~12.1.0" @@ -6421,7 +6421,7 @@ __metadata: yaml: "npm:~2.5.0" bin: lint-staged: bin/lint-staged.js - checksum: 10c0/7d43f11f493d27951c746b4c077fed16ba954c0517cf2fd999034e9e7bf86fde506a797b23531a56a1fde4c24846e0f6583ce6db3bdfd42e92335b1aab367737 + checksum: 10c0/820c622378b62b826974af17f1747e2a4b0556e4fb99d101af89ad298d392ff079f580fdc576f16a27e975d726b95d73495fd524139402ff654c4649ef2f1a6a languageName: node linkType: hard @@ -9920,7 +9920,7 @@ __metadata: json-schema-to-ts: "npm:2.12.0" json2csv: "npm:^5.0.7" knex: "npm:^3.1.0" - lint-staged: "npm:15.2.8" + lint-staged: "npm:15.2.9" lodash.get: "npm:^4.4.2" lodash.groupby: "npm:^4.6.0" lodash.sortby: "npm:^4.7.0" From 3cd312f9a996606646a9a52c683d3c236c85c71a Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Wed, 21 Aug 2024 08:59:19 +0200 Subject: [PATCH 05/13] chore: allow you to use the options object to override *all* the new resource limits (#7938) This allows us to use different limits for enterprise self-hosted and hosted --- src/lib/create-config.ts | 35 +++++++++++++++++++++++++---------- src/lib/types/option.ts | 12 +++++++++++- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/lib/create-config.ts b/src/lib/create-config.ts index 67c089c8c5fa..c3626cfaacc4 100644 --- a/src/lib/create-config.ts +++ b/src/lib/create-config.ts @@ -657,41 +657,56 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig { 1, parseEnvVarNumber( process.env.UNLEASH_FEATURE_ENVIRONMENT_STRATEGIES_LIMIT, - 30, + options?.resourceLimits?.featureEnvironmentStrategies ?? 30, ), ), constraintValues: Math.max( 1, parseEnvVarNumber( process.env.UNLEASH_CONSTRAINT_VALUES_LIMIT, - options?.resourceLimits?.constraintValues || 250, + options?.resourceLimits?.constraintValues ?? 250, ), ), constraints: Math.max( 0, - parseEnvVarNumber(process.env.UNLEASH_CONSTRAINTS_LIMIT, 30), + parseEnvVarNumber( + process.env.UNLEASH_CONSTRAINTS_LIMIT, + options?.resourceLimits?.constraints ?? 30, + ), ), - environments: parseEnvVarNumber( - process.env.UNLEASH_ENVIRONMENTS_LIMIT, - 50, + environments: Math.max( + 1, + parseEnvVarNumber( + process.env.UNLEASH_ENVIRONMENTS_LIMIT, + options?.resourceLimits?.environments ?? 50, + ), ), projects: Math.max( 1, - parseEnvVarNumber(process.env.UNLEASH_PROJECTS_LIMIT, 500), + parseEnvVarNumber( + process.env.UNLEASH_PROJECTS_LIMIT, + options?.resourceLimits?.projects ?? 500, + ), ), apiTokens: Math.max( 0, - parseEnvVarNumber(process.env.UNLEASH_API_TOKENS_LIMIT, 2000), + parseEnvVarNumber( + process.env.UNLEASH_API_TOKENS_LIMIT, + options?.resourceLimits?.apiTokens ?? 2000, + ), ), segments: Math.max( 0, - parseEnvVarNumber(process.env.UNLEASH_SEGMENTS_LIMIT, 300), + parseEnvVarNumber( + process.env.UNLEASH_SEGMENTS_LIMIT, + options?.resourceLimits?.segments ?? 300, + ), ), featureFlags: Math.max( 1, parseEnvVarNumber( process.env.UNLEASH_FEATURE_FLAGS_LIMIT, - options?.resourceLimits?.featureFlags || 5000, + options?.resourceLimits?.featureFlags ?? 5000, ), ), }; diff --git a/src/lib/types/option.ts b/src/lib/types/option.ts index 40220ad3e3d2..08127560604b 100644 --- a/src/lib/types/option.ts +++ b/src/lib/types/option.ts @@ -144,7 +144,17 @@ export interface IUnleashOptions { dailyMetricsStorageDays?: number; rateLimiting?: Partial; resourceLimits?: Partial< - Pick + Pick< + ResourceLimitsSchema, + | 'apiTokens' + | 'constraintValues' + | 'constraints' + | 'environments' + | 'featureEnvironmentStrategies' + | 'featureFlags' + | 'projects' + | 'segments' + > >; } From 89f3f09b6e12df36f6d05e896e8ba044129aa6f0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Aug 2024 09:59:30 +0200 Subject: [PATCH 06/13] chore(deps): bump axios from 1.6.8 to 1.7.4 in /docker (#7878) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [axios](https://github.com/axios/axios) from 1.6.8 to 1.7.4.
Release notes

Sourced from axios's releases.

Release v1.7.4

Release notes:

Bug Fixes

Contributors to this release

Release v1.7.3

Release notes:

Bug Fixes

  • adapter: fix progress event emitting; (#6518) (e3c76fc)
  • fetch: fix withCredentials request config (#6505) (85d4d0e)
  • xhr: return original config on errors from XHR adapter (#6515) (8966ee7)

Contributors to this release

Release v1.7.2

Release notes:

Bug Fixes

Contributors to this release

Release v1.7.1

Release notes:

Bug Fixes

  • fetch: fixed ReferenceError issue when TextEncoder is not available in the environment; (#6410) (733f15f)

Contributors to this release

Release v1.7.0

Release notes:

Features

... (truncated)

Changelog

Sourced from axios's changelog.

1.7.4 (2024-08-13)

Bug Fixes

Contributors to this release

1.7.3 (2024-08-01)

Bug Fixes

  • adapter: fix progress event emitting; (#6518) (e3c76fc)
  • fetch: fix withCredentials request config (#6505) (85d4d0e)
  • xhr: return original config on errors from XHR adapter (#6515) (8966ee7)

Contributors to this release

1.7.2 (2024-05-21)

Bug Fixes

Contributors to this release

1.7.1 (2024-05-20)

Bug Fixes

  • fetch: fixed ReferenceError issue when TextEncoder is not available in the environment; (#6410) (733f15f)

Contributors to this release

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=axios&package-manager=npm_and_yarn&previous-version=1.6.8&new-version=1.7.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/Unleash/unleash/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docker/yarn.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docker/yarn.lock b/docker/yarn.lock index c368763ddf76..ec1d43c0b23c 100644 --- a/docker/yarn.lock +++ b/docker/yarn.lock @@ -669,13 +669,13 @@ __metadata: linkType: hard "axios@npm:^1.6.5": - version: 1.6.8 - resolution: "axios@npm:1.6.8" + version: 1.7.4 + resolution: "axios@npm:1.7.4" dependencies: follow-redirects: "npm:^1.15.6" form-data: "npm:^4.0.0" proxy-from-env: "npm:^1.1.0" - checksum: 10c0/0f22da6f490335479a89878bc7d5a1419484fbb437b564a80c34888fc36759ae4f56ea28d55a191695e5ed327f0bad56e7ff60fb6770c14d1be6501505d47ab9 + checksum: 10c0/5ea1a93140ca1d49db25ef8e1bd8cfc59da6f9220159a944168860ad15a2743ea21c5df2967795acb15cbe81362f5b157fdebbea39d53117ca27658bab9f7f17 languageName: node linkType: hard @@ -4917,7 +4917,7 @@ __metadata: "unleash-server@file:../build::locator=unleash-docker%40workspace%3A.": version: 6.0.4+main - resolution: "unleash-server@file:../build#../build::hash=ac980c&locator=unleash-docker%40workspace%3A." + resolution: "unleash-server@file:../build#../build::hash=46341b&locator=unleash-docker%40workspace%3A." dependencies: "@slack/web-api": "npm:^6.10.0" "@wesleytodd/openapi": "npm:^0.3.0" @@ -4980,7 +4980,7 @@ __metadata: type-is: "npm:^1.6.18" unleash-client: "npm:5.5.5" uuid: "npm:^9.0.0" - checksum: 10c0/4fa1ad68c7db48a1acdb8bf5755f4654bad3b995075616427ecd7cb86a2887028f80c0140542048edbbab81930d9019aa98d0359e38b5fa907455ce5a761e333 + checksum: 10c0/ce1aac9e6d9d81026b5f9e797bbef2e170606e991ab9c4498933ea1ae8a4fcac36a9db8c067ab4a02ac5c91c730d5fcb75c4deb91ead402ffaf7e6ddaa61e205 languageName: node linkType: hard From ee1d8ee8cd08b8c19f75e66d8d325102d71441fb Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Wed, 21 Aug 2024 10:34:13 +0200 Subject: [PATCH 07/13] fix: misc fixes for project archive (#7948) --- .../ArchiveProject/ProjectArchived.tsx | 2 +- .../EditProject/ArchiveProjectForm.tsx | 2 +- .../ReviveProjectDialog.tsx | 3 ++ .../project/project-service.limit.test.ts | 35 ++++++++++++++++++- src/lib/features/project/project-service.ts | 2 ++ 5 files changed, 41 insertions(+), 3 deletions(-) diff --git a/frontend/src/component/project/Project/ArchiveProject/ProjectArchived.tsx b/frontend/src/component/project/Project/ArchiveProject/ProjectArchived.tsx index 4f3fd9d9d321..879859e9d3fd 100644 --- a/frontend/src/component/project/Project/ArchiveProject/ProjectArchived.tsx +++ b/frontend/src/component/project/Project/ArchiveProject/ProjectArchived.tsx @@ -6,7 +6,7 @@ export const ProjectArchived: FC<{ name: string }> = ({ name }) => {

The project {name} has been archived. You can find it on the{' '} - projects archive page. + archive page for projects.

); }; diff --git a/frontend/src/component/project/Project/ProjectSettings/Settings/EditProject/ArchiveProjectForm.tsx b/frontend/src/component/project/Project/ProjectSettings/Settings/EditProject/ArchiveProjectForm.tsx index 6cf9116547f9..4ea77b471c2e 100644 --- a/frontend/src/component/project/Project/ProjectSettings/Settings/EditProject/ArchiveProjectForm.tsx +++ b/frontend/src/component/project/Project/ProjectSettings/Settings/EditProject/ArchiveProjectForm.tsx @@ -18,7 +18,7 @@ export const ArchiveProjectForm = ({ featureCount }: IDeleteProjectForm) => { const { uiConfig } = useUiConfig(); const { loading } = useProjectApi(); const formatProjectArchiveApiCode = () => { - return `curl --location --request DELETE '${uiConfig.unleashUrl}/api/admin/projects/${id}/archive' \\ + return `curl --location --request POST '${uiConfig.unleashUrl}/api/admin/projects/archive/${id}' \\ --header 'Authorization: INSERT_API_KEY' '`; }; diff --git a/frontend/src/component/project/ProjectList/ReviveProjectDialog/ReviveProjectDialog.tsx b/frontend/src/component/project/ProjectList/ReviveProjectDialog/ReviveProjectDialog.tsx index ae59733a3721..1889d846a72e 100644 --- a/frontend/src/component/project/ProjectList/ReviveProjectDialog/ReviveProjectDialog.tsx +++ b/frontend/src/component/project/ProjectList/ReviveProjectDialog/ReviveProjectDialog.tsx @@ -5,6 +5,7 @@ import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi'; import useProjects from 'hooks/api/getters/useProjects/useProjects'; import useToast from 'hooks/useToast'; import { formatUnknownError } from 'utils/formatUnknownError'; +import { useNavigate } from 'react-router-dom'; type ReviveProjectDialogProps = { name: string; @@ -27,6 +28,7 @@ export const ReviveProjectDialog = ({ const { refetch: refetchProjects } = useProjects(); const { refetch: refetchProjectArchive } = useProjects({ archived: true }); const { setToastData, setToastApiError } = useToast(); + const navigate = useNavigate(); const onClick = async (e: React.SyntheticEvent) => { e.preventDefault(); @@ -35,6 +37,7 @@ export const ReviveProjectDialog = ({ await reviveProject(id); refetchProjects(); refetchProjectArchive(); + navigate(`/projects/${id}`); setToastData({ title: 'Revive project', type: 'success', diff --git a/src/lib/features/project/project-service.limit.test.ts b/src/lib/features/project/project-service.limit.test.ts index 8bdc82f529e3..a28accf90107 100644 --- a/src/lib/features/project/project-service.limit.test.ts +++ b/src/lib/features/project/project-service.limit.test.ts @@ -9,7 +9,7 @@ const alwaysOnFlagResolver = { }, } as unknown as IFlagResolver; -test('Should not allow to exceed project limit', async () => { +test('Should not allow to exceed project limit on create', async () => { const LIMIT = 1; const projectService = createFakeProjectService({ ...createTestConfig(), @@ -31,3 +31,36 @@ test('Should not allow to exceed project limit', async () => { "Failed to create project. You can't create more than the established limit of 1.", ); }); + +test('Should not allow to exceed project limit on revive', async () => { + const LIMIT = 1; + const projectService = createFakeProjectService({ + ...createTestConfig(), + flagResolver: alwaysOnFlagResolver, + resourceLimits: { + projects: LIMIT, + }, + eventBus: { + emit: () => {}, + }, + } as unknown as IUnleashConfig); + + const createProject = (name: string) => + projectService.createProject( + { name, id: name }, + {} as IUser, + {} as IAuditUser, + ); + const archiveProject = (id: string) => + projectService.archiveProject(id, {} as IAuditUser); + const reviveProject = (id: string) => + projectService.reviveProject(id, {} as IAuditUser); + + await createProject('projectA'); + await archiveProject('projectA'); + await createProject('projectB'); + + await expect(() => reviveProject('projectA')).rejects.toThrow( + "Failed to create project. You can't create more than the established limit of 1.", + ); +}); diff --git a/src/lib/features/project/project-service.ts b/src/lib/features/project/project-service.ts index 081aba318dad..c5363418bcc6 100644 --- a/src/lib/features/project/project-service.ts +++ b/src/lib/features/project/project-service.ts @@ -634,6 +634,8 @@ export default class ProjectService { } async reviveProject(id: string, auditUser: IAuditUser): Promise { + await this.validateProjectLimit(); + await this.projectStore.revive(id); await this.eventService.storeEvent( From 43100f95614122b9c9864e2e2f350ec5e8248c2e Mon Sep 17 00:00:00 2001 From: "gitar-bot[bot]" <159877585+gitar-bot[bot]@users.noreply.github.com> Date: Wed, 21 Aug 2024 12:01:35 +0200 Subject: [PATCH 08/13] Cleaning up stale flag: insightsV2 with value true (#7896) Co-authored-by: Gitar Co-authored-by: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> --- .../src/component/insights/Insights.test.tsx | 4 +- frontend/src/component/insights/Insights.tsx | 107 +----- .../insights/LegacyInsightsCharts.tsx | 254 -------------- frontend/src/component/insights/chart-info.ts | 85 ----- .../insights/components/Widget/Widget.tsx | 47 --- .../HealthStats/LegacyHealthStats.tsx | 320 ------------------ frontend/src/interfaces/uiConfig.ts | 1 - .../__snapshots__/create-config.test.ts.snap | 1 - src/lib/types/experimental.ts | 5 - src/server-dev.ts | 1 - 10 files changed, 10 insertions(+), 815 deletions(-) delete mode 100644 frontend/src/component/insights/LegacyInsightsCharts.tsx delete mode 100644 frontend/src/component/insights/chart-info.ts delete mode 100644 frontend/src/component/insights/components/Widget/Widget.tsx delete mode 100644 frontend/src/component/insights/componentsStat/HealthStats/LegacyHealthStats.tsx diff --git a/frontend/src/component/insights/Insights.test.tsx b/frontend/src/component/insights/Insights.test.tsx index 6f40943b560d..b8ad1cb0c6ba 100644 --- a/frontend/src/component/insights/Insights.test.tsx +++ b/frontend/src/component/insights/Insights.test.tsx @@ -1,6 +1,6 @@ import { render } from '../../utils/testRenderer'; import { fireEvent, screen } from '@testing-library/react'; -import { NewInsights } from './Insights'; +import { Insights } from './Insights'; import { testServerRoute, testServerSetup } from '../../utils/testServer'; import { vi } from 'vitest'; @@ -30,7 +30,7 @@ const currentTime = '2024-04-25T08:05:00.000Z'; test('Filter insights by project and date', async () => { vi.setSystemTime(currentTime); setupApi(); - render(); + render(); const addFilter = await screen.findByText('Add Filter'); fireEvent.click(addFilter); diff --git a/frontend/src/component/insights/Insights.tsx b/frontend/src/component/insights/Insights.tsx index a543ab1a9bee..5d4987396525 100644 --- a/frontend/src/component/insights/Insights.tsx +++ b/frontend/src/component/insights/Insights.tsx @@ -1,17 +1,11 @@ import { useState, type FC } from 'react'; -import { Box, styled } from '@mui/material'; -import { ArrayParam, withDefault } from 'use-query-params'; +import { styled } from '@mui/material'; import { usePersistentTableState } from 'hooks/usePersistentTableState'; -import { - allOption, - ProjectSelect, -} from 'component/common/ProjectSelect/ProjectSelect'; +import { allOption } from 'component/common/ProjectSelect/ProjectSelect'; import { useInsights } from 'hooks/api/getters/useInsights/useInsights'; import { InsightsHeader } from './components/InsightsHeader/InsightsHeader'; import { useInsightsData } from './hooks/useInsightsData'; -import { type IChartsProps, InsightsCharts } from './InsightsCharts'; -import { LegacyInsightsCharts } from './LegacyInsightsCharts'; -import { useUiFlag } from 'hooks/useUiFlag'; +import { InsightsCharts } from './InsightsCharts'; import { Sticky } from 'component/common/Sticky/Sticky'; import { InsightsFilters } from './InsightsFilters'; import { FilterItemParam } from '../../utils/serializeQueryParams'; @@ -29,87 +23,11 @@ const StickyContainer = styled(Sticky)(({ theme }) => ({ transition: 'padding 0.3s ease', })); -/** - * @deprecated remove with insightsV2 flag - */ -const StickyWrapper = styled(Box, { - shouldForwardProp: (prop) => prop !== 'scrolled', -})<{ scrolled?: boolean }>(({ theme, scrolled }) => ({ - position: 'sticky', - top: 0, - zIndex: theme.zIndex.sticky, - padding: scrolled ? theme.spacing(2, 0) : theme.spacing(2, 0, 2), - background: theme.palette.background.application, - transition: 'padding 0.3s ease', -})); - -const StyledProjectSelect = styled(ProjectSelect)(({ theme }) => ({ - flex: 1, - width: '300px', - [theme.breakpoints.down('sm')]: { - width: '100%', - }, -})); - -/** - * @deprecated remove with insightsV2 flag - */ -const LegacyInsights: FC = () => { - const [scrolled, setScrolled] = useState(false); - const { insights, loading, error } = useInsights(); - const stateConfig = { - projects: withDefault(ArrayParam, [allOption.id]), - }; - const [state, setState] = usePersistentTableState('insights', stateConfig); - const setProjects = (projects: string[]) => { - setState({ projects }); - }; - const projects = state.projects - ? (state.projects.filter(Boolean) as string[]) - : []; - - const insightsData = useInsightsData(insights, projects); - - const handleScroll = () => { - if (!scrolled && window.scrollY > 0) { - setScrolled(true); - } else if (scrolled && window.scrollY === 0) { - setScrolled(false); - } - }; - - if (typeof window !== 'undefined') { - window.addEventListener('scroll', handleScroll); - } - - return ( - - - - } - /> - - - - ); -}; - interface InsightsProps { - ChartComponent?: FC; + withCharts?: boolean; } -export const NewInsights: FC = ({ ChartComponent }) => { +export const Insights: FC = ({ withCharts = true }) => { const [scrolled, setScrolled] = useState(false); const stateConfig = { @@ -118,7 +36,7 @@ export const NewInsights: FC = ({ ChartComponent }) => { to: FilterItemParam, }; const [state, setState] = usePersistentTableState('insights', stateConfig); - const { insights, loading, error } = useInsights( + const { insights, loading } = useInsights( state.from?.values[0], state.to?.values[0], ); @@ -148,8 +66,8 @@ export const NewInsights: FC = ({ ChartComponent }) => { } /> - {ChartComponent && ( - = ({ ChartComponent }) => { ); }; - -export const Insights: FC = () => { - const isInsightsV2Enabled = useUiFlag('insightsV2'); - - if (isInsightsV2Enabled) - return ; - - return ; -}; diff --git a/frontend/src/component/insights/LegacyInsightsCharts.tsx b/frontend/src/component/insights/LegacyInsightsCharts.tsx deleted file mode 100644 index c83d3a82997d..000000000000 --- a/frontend/src/component/insights/LegacyInsightsCharts.tsx +++ /dev/null @@ -1,254 +0,0 @@ -import type { VFC } from 'react'; -import { Box, styled } from '@mui/material'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import { Widget } from './components/Widget/Widget'; -import { UserStats } from './componentsStat/UserStats/UserStats'; -import { UsersChart } from './componentsChart/UsersChart/UsersChart'; -import { UsersPerProjectChart } from './componentsChart/UsersPerProjectChart/UsersPerProjectChart'; -import { FlagStats } from './componentsStat/FlagStats/FlagStats'; -import { FlagsChart } from './componentsChart/FlagsChart/FlagsChart'; -import { FlagsProjectChart } from './componentsChart/FlagsProjectChart/FlagsProjectChart'; -import { LegacyHealthStats } from './componentsStat/HealthStats/LegacyHealthStats'; -import { ProjectHealthChart } from './componentsChart/ProjectHealthChart/ProjectHealthChart'; -import { TimeToProduction } from './componentsStat/TimeToProduction/TimeToProduction'; -import { TimeToProductionChart } from './componentsChart/TimeToProductionChart/TimeToProductionChart'; -import { MetricsSummaryChart } from './componentsChart/MetricsSummaryChart/MetricsSummaryChart'; -import { UpdatesPerEnvironmentTypeChart } from './componentsChart/UpdatesPerEnvironmentTypeChart/UpdatesPerEnvironmentTypeChart'; -import type { - InstanceInsightsSchema, - InstanceInsightsSchemaFlags, - InstanceInsightsSchemaUsers, -} from 'openapi'; -import type { GroupedDataByProject } from './hooks/useGroupedProjectTrends'; -import { allOption } from 'component/common/ProjectSelect/ProjectSelect'; -import { chartInfo } from './chart-info'; -import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; - -interface IChartsProps { - flags: InstanceInsightsSchema['flags']; - flagTrends: InstanceInsightsSchema['flagTrends']; - projectsData: InstanceInsightsSchema['projectFlagTrends']; - groupedProjectsData: GroupedDataByProject< - InstanceInsightsSchema['projectFlagTrends'] - >; - metricsData: InstanceInsightsSchema['metricsSummaryTrends']; - groupedMetricsData: GroupedDataByProject< - InstanceInsightsSchema['metricsSummaryTrends'] - >; - users: InstanceInsightsSchema['users']; - userTrends: InstanceInsightsSchema['userTrends']; - environmentTypeTrends: InstanceInsightsSchema['environmentTypeTrends']; - summary: { - total: number; - active: number; - stale: number; - potentiallyStale: number; - averageUsers: number; - averageHealth?: string; - flagsPerUser?: string; - medianTimeToProduction?: number; - }; - loading: boolean; - projects: string[]; - allMetricsDatapoints: string[]; -} - -const StyledGrid = styled(Box)(({ theme }) => ({ - display: 'grid', - gridTemplateColumns: `repeat(2, 1fr)`, - gridAutoRows: 'auto', - gap: theme.spacing(2), - paddingBottom: theme.spacing(2), - [theme.breakpoints.up('md')]: { - gridTemplateColumns: `300px 1fr`, - }, -})); - -const ChartWidget = styled(Widget)(({ theme }) => ({ - [theme.breakpoints.down('md')]: { - gridColumnStart: 'span 2', - order: 2, - }, -})); - -/** - * @deprecated remove with insightsV2 flag - */ -export const LegacyInsightsCharts: VFC = ({ - projects, - flags, - users, - summary, - userTrends, - groupedProjectsData, - flagTrends, - groupedMetricsData, - environmentTypeTrends, - allMetricsDatapoints, - loading, -}) => { - const { isEnterprise } = useUiConfig(); - const showAllProjects = projects[0] === allOption.id; - const isOneProjectSelected = projects.length === 1; - - function getFlagsPerUser( - flags: InstanceInsightsSchemaFlags, - users: InstanceInsightsSchemaUsers, - ) { - const flagsPerUserCalculation = flags.total / users.total; - return Number.isNaN(flagsPerUserCalculation) - ? 'N/A' - : flagsPerUserCalculation.toFixed(2); - } - - return ( - <> - - - - - } - elseShow={ - - - - } - /> - - - - } - elseShow={ - - - - } - /> - - - - - - - } - elseShow={ - - - - } - /> - - - - - - - - - - - - - - - } - /> - - - - - - theme.spacing(2) }} - > - - - - } - /> - - ); -}; diff --git a/frontend/src/component/insights/chart-info.ts b/frontend/src/component/insights/chart-info.ts deleted file mode 100644 index dcd517ab0e9e..000000000000 --- a/frontend/src/component/insights/chart-info.ts +++ /dev/null @@ -1,85 +0,0 @@ -/** - * @deprecated remove with insightsV2 flag - */ -export const chartInfo = { - totalUsers: { - title: 'Total users', - tooltip: 'Total number of current users.', - }, - usersInProject: { - title: 'Users in project', - tooltip: 'Average number of users for selected projects.', - }, - avgUsersPerProject: { - title: 'Users per project on average', - tooltip: 'Number of users in selected projects.', - }, - users: { - title: 'Users', - tooltip: 'How the number of users changes over time.', - }, - usersPerProject: { - title: 'Users per project', - tooltip: - 'How the number of users changes over time for the selected projects.', - }, - totalFlags: { - title: 'Total flags', - tooltip: - 'Active flags (not archived) that currently exist across the selected projects.', - }, - flags: { - title: 'Number of flags', - tooltip: - 'How the number of flags has changed over time across all projects.', - }, - flagsPerProject: { - title: 'Flags per project', - tooltip: - 'How the number of flags changes over time for the selected projects.', - }, - averageHealth: { - title: 'Average health', - tooltip: - 'Average health is the current percentage of flags in the selected projects that are not stale or potentially stale.', - }, - overallHealth: { - title: 'Overall Health', - tooltip: - 'How the overall health changes over time across all projects.', - }, - healthPerProject: { - title: 'Health per project', - tooltip: - 'How the overall health changes over time for the selected projects.', - }, - medianTimeToProduction: { - title: 'Median time to production', - tooltip: - 'How long does it currently take on average from when a feature flag was created until it was enabled in a "production" type environment. This is calculated only from feature flags of the type "release" and is the median across the selected projects.', - }, - timeToProduction: { - title: 'Time to production', - tooltip: - 'How the median time to production changes over time across all projects.', - }, - timeToProductionPerProject: { - title: 'Time to production per project', - tooltip: - 'How the average time to production changes over time for the selected projects.', - }, - metrics: { - title: 'Flag evaluation metrics', - tooltip: - 'Summary of all flag evaluations reported by SDKs across all projects.', - }, - metricsPerProject: { - title: 'Flag evaluation metrics per project', - tooltip: - 'Summary of all flag evaluations reported by SDKs for the selected projects.', - }, - updates: { - title: 'Updates per environment type', - tooltip: 'Summary of all configuration updates per environment type.', - }, -}; diff --git a/frontend/src/component/insights/components/Widget/Widget.tsx b/frontend/src/component/insights/components/Widget/Widget.tsx deleted file mode 100644 index 202b3f70b070..000000000000 --- a/frontend/src/component/insights/components/Widget/Widget.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import type React from 'react'; -import type { FC, ReactNode } from 'react'; -import { Paper, Typography, styled, type SxProps } from '@mui/material'; -import { HelpIcon } from 'component/common/HelpIcon/HelpIcon'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import type { Theme } from '@mui/material/styles/createTheme'; -import InfoOutlined from '@mui/icons-material/InfoOutlined'; - -const StyledPaper = styled(Paper)(({ theme }) => ({ - padding: theme.spacing(3), - borderRadius: `${theme.shape.borderRadiusLarge}px`, - minWidth: 0, // bugfix, see: https://github.com/chartjs/Chart.js/issues/4156#issuecomment-295180128 - position: 'relative', -})); - -/** - * @deprecated remove with insightsV2 flag - */ -export const Widget: FC<{ - title: ReactNode; - tooltip?: ReactNode; - sx?: SxProps; - children?: React.ReactNode; -}> = ({ title, children, tooltip, ...rest }) => ( - - ({ - marginBottom: theme.spacing(3), - display: 'flex', - alignItems: 'center', - gap: theme.spacing(0.5), - })} - > - {title} - - - - } - /> - - {children} - -); diff --git a/frontend/src/component/insights/componentsStat/HealthStats/LegacyHealthStats.tsx b/frontend/src/component/insights/componentsStat/HealthStats/LegacyHealthStats.tsx deleted file mode 100644 index 962057d52e43..000000000000 --- a/frontend/src/component/insights/componentsStat/HealthStats/LegacyHealthStats.tsx +++ /dev/null @@ -1,320 +0,0 @@ -import type { FC } from 'react'; -import { useThemeMode } from 'hooks/useThemeMode'; -import { styled, useTheme } from '@mui/material'; - -interface IHealthStatsProps { - value?: string | number; - healthy: number; - stale: number; - potentiallyStale: number; -} - -const StyledSvg = styled('svg')(() => ({ - maxWidth: '250px', - margin: '0 auto', -})); - -/** - * @deprecated remove with insightsV2 flag - */ -export const LegacyHealthStats: FC = ({ - value, - healthy, - stale, - potentiallyStale, -}) => { - const { themeMode } = useThemeMode(); - const isDark = themeMode === 'dark'; - const theme = useTheme(); - - return ( - - Health Stats - - - - - - {value !== undefined ? `${value}%` : 'N/A'} - - - - - - {healthy || 0} - - - Healthy - - - - - - {stale || 0} - - - Stale - - - - - - {potentiallyStale || 0} - - - - Potentially - - - stale - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts index c00a70a7cab6..ad82100c9d50 100644 --- a/frontend/src/interfaces/uiConfig.ts +++ b/frontend/src/interfaces/uiConfig.ts @@ -89,7 +89,6 @@ export type UiFlags = { navigationSidebar?: boolean; flagCreator?: boolean; resourceLimits?: boolean; - insightsV2?: boolean; integrationEvents?: boolean; newEventSearch?: boolean; archiveProjects?: boolean; diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index d5f1617b9b68..1ef0c6659232 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -120,7 +120,6 @@ exports[`should create default config 1`] = ` }, "filterInvalidClientMetrics": false, "googleAuthEnabled": false, - "insightsV2": false, "integrationEvents": false, "killInsightsUI": false, "killScheduledChangeRequestCache": false, diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index e6df34fecd5a..b8a925218016 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -59,7 +59,6 @@ export type IFlagKey = | 'resourceLimits' | 'extendedMetrics' | 'removeUnsafeInlineStyleSrc' - | 'insightsV2' | 'integrationEvents' | 'originMiddleware' | 'newEventSearch' @@ -292,10 +291,6 @@ const flags: IFlags = { process.env.UNLEASH_EXPERIMENTAL_REMOVE_UNSAFE_INLINE_STYLE_SRC, false, ), - insightsV2: parseEnvVarBoolean( - process.env.UNLEASH_EXPERIMENTAL_INSIGHTS_V2, - false, - ), integrationEvents: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_INTEGRATION_EVENTS, false, diff --git a/src/server-dev.ts b/src/server-dev.ts index 33037fd15747..31ad0c5ecd4a 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -52,7 +52,6 @@ process.nextTick(async () => { enableLegacyVariants: false, resourceLimits: true, extendedMetrics: true, - insightsV2: true, integrationEvents: true, originMiddleware: true, newEventSearch: true, From 6c5ce52470a16e9f4d5ffdfb782de21b14f52866 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Wed, 21 Aug 2024 12:03:03 +0200 Subject: [PATCH 09/13] Refactor: Remove `react-timeago` (#7943) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove dependency 🫡 --- .../application/ApplicationChart.tsx | 16 +- .../application/ApplicationOverview.test.tsx | 8 +- .../FeatureArchivedCell.tsx | 13 +- .../ChangeRequestComment.tsx | 9 +- .../ChangeRequestHeader.tsx | 10 +- .../common/Notifications/Notification.tsx | 8 +- .../cells/FeatureSeenCell/FeatureSeenCell.tsx | 131 ----------------- .../cells/FeatureSeenCell/LastSeenTooltip.tsx | 81 +++------- .../common/Table/cells/TextCell/TextCell.tsx | 9 +- .../Table/cells/TimeAgoCell/TimeAgoCell.tsx | 13 +- .../component/common/TimeAgo/TimeAgo.test.tsx | 139 ++++++++++++++++++ .../src/component/common/TimeAgo/TimeAgo.tsx | 73 +++++++++ .../FeatureEnvironmentSeen.tsx | 68 ++++----- .../useLastSeenColors.ts | 58 +++++--- .../FeatureLifecycleTooltip.tsx | 26 +--- .../NewProjectCard/ProjectArchiveCard.tsx | 10 +- frontend/src/interfaces/uiConfig.ts | 1 + .../__snapshots__/create-config.test.ts.snap | 1 + src/lib/types/experimental.ts | 7 +- src/server-dev.ts | 1 + 20 files changed, 352 insertions(+), 330 deletions(-) delete mode 100644 frontend/src/component/common/Table/cells/FeatureSeenCell/FeatureSeenCell.tsx create mode 100644 frontend/src/component/common/TimeAgo/TimeAgo.test.tsx create mode 100644 frontend/src/component/common/TimeAgo/TimeAgo.tsx diff --git a/frontend/src/component/application/ApplicationChart.tsx b/frontend/src/component/application/ApplicationChart.tsx index e8d4089e0b00..c76a7ea64ba4 100644 --- a/frontend/src/component/application/ApplicationChart.tsx +++ b/frontend/src/component/application/ApplicationChart.tsx @@ -13,7 +13,7 @@ import CheckCircle from '@mui/icons-material/CheckCircle'; import CloudCircle from '@mui/icons-material/CloudCircle'; import Flag from '@mui/icons-material/Flag'; import WarningAmberRounded from '@mui/icons-material/WarningAmberRounded'; -import TimeAgo from 'react-timeago'; +import { TimeAgo } from 'component/common/TimeAgo/TimeAgo'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import { getApplicationIssues } from './ApplicationIssues/ApplicationIssues'; @@ -305,17 +305,9 @@ export const ApplicationChart = ({ data }: IApplicationChartProps) => { Last seen: - {environment.lastSeen && ( - - )} + diff --git a/frontend/src/component/application/ApplicationOverview.test.tsx b/frontend/src/component/application/ApplicationOverview.test.tsx index c1db878abb98..7d029130b411 100644 --- a/frontend/src/component/application/ApplicationOverview.test.tsx +++ b/frontend/src/component/application/ApplicationOverview.test.tsx @@ -13,7 +13,11 @@ const setupApi = (application: ApplicationOverviewSchema) => { '/api/admin/metrics/applications/my-app/overview', application, ); - testServerRoute(server, '/api/admin/ui-config', {}); + testServerRoute(server, '/api/admin/ui-config', { + flags: { + timeAgoRefactor: true, + }, + }); }; test('Display application overview with environments', async () => { @@ -51,7 +55,7 @@ test('Display application overview with environments', async () => { await screen.findByText('development environment'); await screen.findByText('999'); await screen.findByText('unleash-client-node:5.5.0-beta.0'); - await screen.findByText('0 seconds ago'); + await screen.findByText('< 1 minute ago'); }); test('Display application overview without environments', async () => { diff --git a/frontend/src/component/archive/ArchiveTable/FeatureArchivedCell/FeatureArchivedCell.tsx b/frontend/src/component/archive/ArchiveTable/FeatureArchivedCell/FeatureArchivedCell.tsx index 3f2864f46b48..16f97c3e272e 100644 --- a/frontend/src/component/archive/ArchiveTable/FeatureArchivedCell/FeatureArchivedCell.tsx +++ b/frontend/src/component/archive/ArchiveTable/FeatureArchivedCell/FeatureArchivedCell.tsx @@ -1,5 +1,5 @@ -import type { VFC } from 'react'; -import TimeAgo from 'react-timeago'; +import type { FC } from 'react'; +import { TimeAgo } from 'component/common/TimeAgo/TimeAgo'; import { Tooltip, Typography, useTheme } from '@mui/material'; import { formatDateYMD } from 'utils/formatDate'; import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; @@ -9,7 +9,7 @@ interface IFeatureArchivedCellProps { value?: string | Date | null; } -export const FeatureArchivedCell: VFC = ({ +export const FeatureArchivedCell: FC = ({ value: archivedAt, }) => { const { locationSettings } = useLocationSettings(); @@ -37,12 +37,7 @@ export const FeatureArchivedCell: VFC = ({ arrow > - + diff --git a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestComments/ChangeRequestComment.tsx b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestComments/ChangeRequestComment.tsx index 3f63465fffbb..c82232173607 100644 --- a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestComments/ChangeRequestComment.tsx +++ b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestComments/ChangeRequestComment.tsx @@ -2,7 +2,7 @@ import type { FC } from 'react'; import { Markdown } from 'component/common/Markdown/Markdown'; import Paper from '@mui/material/Paper'; import { Box, styled, Typography } from '@mui/material'; -import TimeAgo from 'react-timeago'; +import { TimeAgo } from 'component/common/TimeAgo/TimeAgo'; import { StyledAvatar } from './StyledAvatar'; import type { IChangeRequestComment } from '../../changeRequest.types'; @@ -35,12 +35,7 @@ export const ChangeRequestComment: FC<{ comment: IChangeRequestComment }> = ({ {comment.createdBy.username}{' '} - commented{' '} - + commented diff --git a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestHeader/ChangeRequestHeader.tsx b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestHeader/ChangeRequestHeader.tsx index e5c3d4aac5f7..32038ea6af74 100644 --- a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestHeader/ChangeRequestHeader.tsx +++ b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestHeader/ChangeRequestHeader.tsx @@ -1,7 +1,7 @@ import { Box } from '@mui/material'; import { type FC, useState } from 'react'; import { Typography, Tooltip } from '@mui/material'; -import TimeAgo from 'react-timeago'; +import { TimeAgo } from 'component/common/TimeAgo/TimeAgo'; import type { ChangeRequestType } from 'component/changeRequest/changeRequest.types'; import { ChangeRequestStatusBadge } from 'component/changeRequest/ChangeRequestStatusBadge/ChangeRequestStatusBadge'; import { @@ -38,13 +38,7 @@ export const ChangeRequestHeader: FC<{ changeRequest: ChangeRequestType }> = ({ margin: theme.spacing('auto', 0, 'auto', 2), })} > - Created{' '} - {' '} - by + Created by ({ diff --git a/frontend/src/component/common/Notifications/Notification.tsx b/frontend/src/component/common/Notifications/Notification.tsx index 6b584507d17b..6797105cadbc 100644 --- a/frontend/src/component/common/Notifications/Notification.tsx +++ b/frontend/src/component/common/Notifications/Notification.tsx @@ -11,7 +11,7 @@ import type { NotificationsSchemaItemNotificationType, } from 'openapi'; import { ReactComponent as ChangesAppliedIcon } from 'assets/icons/merge.svg'; -import TimeAgo from 'react-timeago'; +import { TimeAgo } from 'component/common/TimeAgo/TimeAgo'; import ToggleOffOutlined from '@mui/icons-material/ToggleOffOutlined'; import { flexRow } from 'themes/themeStyles'; @@ -157,11 +157,7 @@ export const Notification = ({ - + diff --git a/frontend/src/component/common/Table/cells/FeatureSeenCell/FeatureSeenCell.tsx b/frontend/src/component/common/Table/cells/FeatureSeenCell/FeatureSeenCell.tsx deleted file mode 100644 index 98fba47e812f..000000000000 --- a/frontend/src/component/common/Table/cells/FeatureSeenCell/FeatureSeenCell.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import type React from 'react'; -import type { FC, VFC } from 'react'; -import TimeAgo from 'react-timeago'; -import { styled, Tooltip, useTheme } from '@mui/material'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; - -function shortenUnitName(unit?: string): string { - switch (unit) { - case 'second': - return 's'; - case 'minute': - return 'm'; - case 'hour': - return 'h'; - case 'day': - return 'D'; - case 'week': - return 'W'; - case 'month': - return 'M'; - case 'year': - return 'Y'; - default: - return ''; - } -} - -const useFeatureColor = () => { - const theme = useTheme(); - - return (unit?: string): string => { - switch (unit) { - case 'second': - return theme.palette.seen.recent; - case 'minute': - return theme.palette.seen.recent; - case 'hour': - return theme.palette.seen.recent; - case 'day': - return theme.palette.seen.recent; - case 'week': - return theme.palette.seen.inactive; - case 'month': - return theme.palette.seen.abandoned; - case 'year': - return theme.palette.seen.abandoned; - default: - return theme.palette.seen.unknown; - } - }; -}; - -const StyledContainer = styled('div')(({ theme }) => ({ - display: 'flex', - padding: theme.spacing(1.5), -})); - -const StyledBox = styled('div')(({ theme }) => ({ - width: '38px', - height: '38px', - background: theme.palette.background.paper, - borderRadius: `${theme.shape.borderRadius}px`, - textAlign: 'center', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - fontSize: theme.typography.body2.fontSize, - margin: '0 auto', -})); - -interface IFeatureSeenCellProps { - value?: string | Date | null; -} - -const Wrapper: FC<{ - unit?: string; - tooltip: string; - children?: React.ReactNode; -}> = ({ unit, tooltip, children }) => { - const getColor = useFeatureColor(); - - return ( - - - - {children} - - - - ); -}; - -export const FeatureSeenCell: VFC = ({ - value: lastSeenAt, -}) => { - return ( - { - return ( - - {value} - {shortenUnitName(unit)} - - ); - }} - /> - } - elseShow={ - - – - - } - /> - ); -}; diff --git a/frontend/src/component/common/Table/cells/FeatureSeenCell/LastSeenTooltip.tsx b/frontend/src/component/common/Table/cells/FeatureSeenCell/LastSeenTooltip.tsx index db09b5470d87..cc40260e52d8 100644 --- a/frontend/src/component/common/Table/cells/FeatureSeenCell/LastSeenTooltip.tsx +++ b/frontend/src/component/common/Table/cells/FeatureSeenCell/LastSeenTooltip.tsx @@ -1,5 +1,5 @@ import { styled, type SxProps, type Theme, Typography } from '@mui/material'; -import TimeAgo from 'react-timeago'; +import { TimeAgo } from 'component/common/TimeAgo/TimeAgo'; import type { ILastSeenEnvironments } from 'interfaces/featureToggle'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { useLastSeenColors } from 'component/feature/FeatureView/FeatureEnvironmentSeen/useLastSeenColors'; @@ -74,10 +74,10 @@ export const LastSeenTooltip = ({ ...rest }: ILastSeenTooltipProps) => { const getColor = useLastSeenColors(); - const [, defaultTextColor] = getColor(); const environmentsHaveLastSeen = environments?.some((environment) => Boolean(environment.lastSeenAt), ); + return ( @@ -85,7 +85,9 @@ export const LastSeenTooltip = ({ @@ -95,43 +97,15 @@ export const LastSeenTooltip = ({ {name} - { - const [, textColor] = - getColor(unit); - return ( - - {`${value} ${unit}${ - value !== 1 - ? 's' - : '' - } ${suffix}`} - - ); - }} - /> - } - elseShow={ - - no usage - - } - /> + + + @@ -139,27 +113,12 @@ export const LastSeenTooltip = ({ } elseShow={ - { - return ( - - {`Reported ${value} ${unit}${ - value !== 1 ? 's' : '' - } ${suffix}`} - - ); - }} - /> + + Reported + } /> diff --git a/frontend/src/component/common/Table/cells/TextCell/TextCell.tsx b/frontend/src/component/common/Table/cells/TextCell/TextCell.tsx index b97389264b2d..98fd97fe2a76 100644 --- a/frontend/src/component/common/Table/cells/TextCell/TextCell.tsx +++ b/frontend/src/component/common/Table/cells/TextCell/TextCell.tsx @@ -24,6 +24,11 @@ const StyledWrapper = styled(Box, { }, })); +const StyledSpan = styled('span')(() => ({ + display: 'inline-block', + maxWidth: '100%', +})); + export const TextCell: FC = ({ value, children, @@ -32,8 +37,8 @@ export const TextCell: FC = ({ 'data-testid': testid, }) => ( - + {children ?? value} - + ); diff --git a/frontend/src/component/common/Table/cells/TimeAgoCell/TimeAgoCell.tsx b/frontend/src/component/common/Table/cells/TimeAgoCell/TimeAgoCell.tsx index 0cee4dd95539..02aa222878ce 100644 --- a/frontend/src/component/common/Table/cells/TimeAgoCell/TimeAgoCell.tsx +++ b/frontend/src/component/common/Table/cells/TimeAgoCell/TimeAgoCell.tsx @@ -3,7 +3,7 @@ import { useLocationSettings } from 'hooks/useLocationSettings'; import type { FC } from 'react'; import { formatDateYMD } from 'utils/formatDate'; import { TextCell } from '../TextCell/TextCell'; -import TimeAgo from 'react-timeago'; +import { TimeAgo } from 'component/common/TimeAgo/TimeAgo'; interface ITimeAgoCellProps { value?: string | number | Date; @@ -31,16 +31,15 @@ export const TimeAgoCell: FC = ({ - + diff --git a/frontend/src/component/common/TimeAgo/TimeAgo.test.tsx b/frontend/src/component/common/TimeAgo/TimeAgo.test.tsx new file mode 100644 index 000000000000..1ec64f48c98c --- /dev/null +++ b/frontend/src/component/common/TimeAgo/TimeAgo.test.tsx @@ -0,0 +1,139 @@ +import { vi } from 'vitest'; +import { act, render, screen } from '@testing-library/react'; +import { NewTimeAgo as TimeAgo } from './TimeAgo'; + +const h = 3_600_000 as const; +const min = 60_000 as const; +const s = 1_000 as const; + +beforeAll(() => { + vi.useFakeTimers(); +}); + +afterAll(() => { + vi.useRealTimers(); +}); + +test('renders fallback when date is null or undefined', () => { + render(); + expect(screen.getByText('N/A')).toBeInTheDocument(); + + render(); + expect(screen.getByText('unknown')).toBeInTheDocument(); +}); + +test('formats date correctly', () => { + const date = new Date(); + render(); + expect(screen.getByText('< 1 minute ago')).toBeInTheDocument(); +}); + +test('updates time periodically', () => { + const date = new Date(); + render(); + + expect(screen.getByText('< 1 minute ago')).toBeInTheDocument(); + + act(() => vi.advanceTimersByTime(61 * s)); + + expect(screen.getByText('1 minute ago')).toBeInTheDocument(); +}); + +test('stops updating when live is false', () => { + const date = new Date(); + const setIntervalSpy = vi.spyOn(global, 'setInterval'); + render(); + + expect(screen.getByText('< 1 minute ago')).toBeInTheDocument(); + + act(() => vi.advanceTimersByTime(61 * s)); + + expect(screen.getByText('< 1 minute ago')).toBeInTheDocument(); + + expect(setIntervalSpy).not.toHaveBeenCalled(); +}); + +test('handles string dates', () => { + const dateString = '2024-01-01T00:00:00Z'; + vi.setSystemTime(new Date('2024-01-01T01:01:00Z')); + + render(); + expect(screen.getByText('1 hour ago')).toBeInTheDocument(); +}); + +test('cleans up interval on unmount', () => { + const date = new Date(); + const { unmount } = render(); + + const clearIntervalSpy = vi.spyOn(global, 'clearInterval'); + unmount(); + expect(clearIntervalSpy).toHaveBeenCalled(); +}); + +test('renders fallback for invalid date', () => { + render(); + expect(screen.getByText('Invalid date')).toBeInTheDocument(); +}); + +test('on date change, current time should be updated', () => { + const start = new Date().getTime(); + + vi.advanceTimersByTime(60_000); + + const Component = ({ date }: { date: number }) => ( + <> + + + ); + + const { rerender } = render(); + expect(screen.getByText('1 minute ago')).toBeInTheDocument(); + + act(() => vi.advanceTimersByTime(2 * min)); + rerender(); + + expect(screen.getByText('3 minutes ago')).toBeInTheDocument(); + + rerender(); + + expect(screen.getByText('2 minutes ago')).toBeInTheDocument(); +}); + +test('should refresh on fallback change', () => { + const date = null; + const { rerender } = render( + , + ); + expect(screen.getByText('Initial fallback')).toBeInTheDocument(); + + rerender(); + expect(screen.getByText('Updated fallback')).toBeInTheDocument(); +}); + +test('should create `time` element', () => { + const now = 1724222592978; + vi.setSystemTime(now); + const { container } = render(); + expect(container).toMatchInlineSnapshot(` +
+ +
+ `); +}); + +test('should not create `time` element if `timeElement` is false', () => { + const now = 1724222592978; + vi.setSystemTime(now); + const { container } = render( + , + ); + expect(container).toMatchInlineSnapshot(` +
+ 5 hours ago +
+ `); +}); diff --git a/frontend/src/component/common/TimeAgo/TimeAgo.tsx b/frontend/src/component/common/TimeAgo/TimeAgo.tsx new file mode 100644 index 000000000000..0c04e7498160 --- /dev/null +++ b/frontend/src/component/common/TimeAgo/TimeAgo.tsx @@ -0,0 +1,73 @@ +import { useEffect, useState, type FC } from 'react'; +import { formatDistanceToNow, secondsToMilliseconds } from 'date-fns'; +import { default as LegacyTimeAgo } from 'react-timeago'; +import { useUiFlag } from 'hooks/useUiFlag'; + +type TimeAgoProps = { + date: Date | number | string | null | undefined; + fallback?: string; + refresh?: boolean; + timeElement?: boolean; +}; + +const formatTimeAgo = (date: string | number | Date) => + formatDistanceToNow(new Date(date), { + addSuffix: true, + }) + .replace('about ', '') + .replace('less than a minute ago', '< 1 minute ago'); + +export const TimeAgo: FC = ({ ...props }) => { + const { date, fallback, refresh } = props; + const timeAgoRefactorEnabled = useUiFlag('timeAgoRefactor'); + + if (timeAgoRefactorEnabled) return ; + if (!date) return fallback; + return ( + + ); +}; + +export const NewTimeAgo: FC = ({ + date, + fallback = '', + refresh = true, + timeElement = true, +}) => { + const getValue = (): { description: string; dateTime?: Date } => { + try { + if (!date) return { description: fallback }; + return { + description: formatTimeAgo(date), + dateTime: timeElement ? new Date(date) : undefined, + }; + } catch { + return { description: fallback }; + } + }; + const [state, setState] = useState(getValue); + + useEffect(() => { + setState(getValue); + }, [date, fallback]); + + useEffect(() => { + if (!date || !refresh) return; + + const intervalId = setInterval(() => { + setState(getValue); + }, secondsToMilliseconds(12)); + + return () => clearInterval(intervalId); + }, [refresh]); + + if (!state.dateTime) { + return state.description; + } + + return ( + + ); +}; + +export default TimeAgo; diff --git a/frontend/src/component/feature/FeatureView/FeatureEnvironmentSeen/FeatureEnvironmentSeen.tsx b/frontend/src/component/feature/FeatureView/FeatureEnvironmentSeen/FeatureEnvironmentSeen.tsx index 28583bba897e..5ebdafc6d566 100644 --- a/frontend/src/component/feature/FeatureView/FeatureEnvironmentSeen/FeatureEnvironmentSeen.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureEnvironmentSeen/FeatureEnvironmentSeen.tsx @@ -1,4 +1,3 @@ -import TimeAgo from 'react-timeago'; import { LastSeenTooltip } from 'component/common/Table/cells/FeatureSeenCell/LastSeenTooltip'; import type React from 'react'; import type { FC, ReactElement } from 'react'; @@ -85,45 +84,36 @@ export const FeatureEnvironmentSeen = ({ const lastSeen = getLatestLastSeenAt(environments) || featureLastSeen; + if (!lastSeen) { + return ( + + + + + + + + ); + } + + const { background, text } = getColor(lastSeen); + return ( - <> - {lastSeen ? ( - { - const [color, textColor] = getColor(unit); - return ( - - } - color={color} - > - - - ); - }} + - ) : ( - - - - - - - - )} - + } + color={background} + > + + ); }; diff --git a/frontend/src/component/feature/FeatureView/FeatureEnvironmentSeen/useLastSeenColors.ts b/frontend/src/component/feature/FeatureView/FeatureEnvironmentSeen/useLastSeenColors.ts index a069f0bee32c..2bcba0221fb0 100644 --- a/frontend/src/component/feature/FeatureView/FeatureEnvironmentSeen/useLastSeenColors.ts +++ b/frontend/src/component/feature/FeatureView/FeatureEnvironmentSeen/useLastSeenColors.ts @@ -1,25 +1,47 @@ import { useTheme } from '@mui/material'; +import { differenceInDays } from 'date-fns'; -export const useLastSeenColors = () => { +type Color = { + background: string; + text: string; +}; + +export const useLastSeenColors = (): (( + date?: Date | number | string | null, +) => Color) => { const theme = useTheme(); + const colorsForUnknown = { + background: theme.palette.seen.unknown, + text: theme.palette.grey.A400, + }; - return (unit?: string): [string, string] => { - switch (unit) { - case 'second': - case 'minute': - case 'hour': - case 'day': - return [theme.palette.seen.recent, theme.palette.success.main]; - case 'week': - return [ - theme.palette.seen.inactive, - theme.palette.warning.main, - ]; - case 'month': - case 'year': - return [theme.palette.seen.abandoned, theme.palette.error.main]; - default: - return [theme.palette.seen.unknown, theme.palette.grey.A400]; + return (date?: Date | number | string | null): Color => { + if (!date) { + return colorsForUnknown; } + + try { + const days = differenceInDays(Date.now(), new Date(date)); + + if (days < 1) { + return { + background: theme.palette.seen.recent, + text: theme.palette.success.main, + }; + } + if (days <= 7) { + return { + background: theme.palette.seen.inactive, + text: theme.palette.warning.main, + }; + } + + return { + background: theme.palette.seen.abandoned, + text: theme.palette.error.main, + }; + } catch {} + + return colorsForUnknown; }; }; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.tsx index eabaa5996642..9f262a51cf25 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.tsx @@ -11,7 +11,7 @@ import { ReactComponent as ArchivedStageIcon } from 'assets/icons/stage-archived import CloudCircle from '@mui/icons-material/CloudCircle'; import { ReactComponent as UsageRate } from 'assets/icons/usage-rate.svg'; import { FeatureLifecycleStageIcon } from './FeatureLifecycleStageIcon'; -import TimeAgo from 'react-timeago'; +import { TimeAgo } from 'component/common/TimeAgo/TimeAgo'; import { StyledIconWrapper } from '../../FeatureEnvironmentSeen/FeatureEnvironmentSeen'; import { useLastSeenColors } from '../../FeatureEnvironmentSeen/useLastSeenColors'; import type { LifecycleStage } from './LifecycleStage'; @@ -114,22 +114,12 @@ const LastSeenIcon: FC<{ lastSeen: string; }> = ({ lastSeen }) => { const getColor = useLastSeenColors(); + const { text, background } = getColor(lastSeen); return ( - { - const [color, textColor] = getColor(unit); - return ( - - - - ); - }} - /> + + + ); }; @@ -230,11 +220,7 @@ const Environments: FC<{ {environment.name} - + diff --git a/frontend/src/component/project/NewProjectCard/ProjectArchiveCard.tsx b/frontend/src/component/project/NewProjectCard/ProjectArchiveCard.tsx index 0acd011b688b..b0123a7a15b8 100644 --- a/frontend/src/component/project/NewProjectCard/ProjectArchiveCard.tsx +++ b/frontend/src/component/project/NewProjectCard/ProjectArchiveCard.tsx @@ -17,7 +17,6 @@ import { formatDateYMDHM } from 'utils/formatDate'; import { useLocationSettings } from 'hooks/useLocationSettings'; import { parseISO } from 'date-fns'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import TimeAgo from 'react-timeago'; import { Box, Link, Tooltip } from '@mui/material'; import { Link as RouterLink } from 'react-router-dom'; import { @@ -29,6 +28,7 @@ import PermissionIconButton from 'component/common/PermissionIconButton/Permissi import Delete from '@mui/icons-material/Delete'; import { Highlighter } from 'component/common/Highlighter/Highlighter'; import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; +import { TimeAgo } from 'component/common/TimeAgo/TimeAgo'; export type ProjectArchiveCardProps = { id: string; @@ -91,12 +91,8 @@ export const ProjectArchiveCard: FC = ({

Archived:{' '}

diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts index ad82100c9d50..f8e114dc1ab0 100644 --- a/frontend/src/interfaces/uiConfig.ts +++ b/frontend/src/interfaces/uiConfig.ts @@ -93,6 +93,7 @@ export type UiFlags = { newEventSearch?: boolean; archiveProjects?: boolean; projectListImprovements?: boolean; + timeAgoRefactor?: boolean; }; export interface IVersionInfo { diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index 1ef0c6659232..b09129ae1027 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -149,6 +149,7 @@ exports[`should create default config 1`] = ` "showInactiveUsers": false, "signals": false, "strictSchemaValidation": false, + "timeAgoRefactor": false, "useMemoizedActiveTokens": false, "useProjectReadModel": false, "userAccessUIEnabled": false, diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index b8a925218016..6312a2b56675 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -66,7 +66,8 @@ export type IFlagKey = | 'projectListImprovements' | 'useProjectReadModel' | 'webhookServiceNameLogging' - | 'addonUsageMetrics'; + | 'addonUsageMetrics' + | 'timeAgoRefactor'; export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; @@ -323,6 +324,10 @@ const flags: IFlags = { process.env.UNLEASH_EXPERIMENTAL_ADDON_USAGE_METRICS, false, ), + timeAgoRefactor: parseEnvVarBoolean( + process.env.UNLEASH_TIMEAGO_REFACTOR, + false, + ), }; export const defaultExperimentalOptions: IExperimentalOptions = { diff --git a/src/server-dev.ts b/src/server-dev.ts index 31ad0c5ecd4a..1d8860a96e84 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -59,6 +59,7 @@ process.nextTick(async () => { useProjectReadModel: true, webhookServiceNameLogging: true, addonUsageMetrics: true, + timeAgoRefactor: true, }, }, authentication: { From df73c65073b6fe5121cd2cdcfc9071dfc84977c2 Mon Sep 17 00:00:00 2001 From: Jaanus Sellin Date: Wed, 21 Aug 2024 13:59:24 +0300 Subject: [PATCH 10/13] feat: filter projectless events for normal users (#7914) Now events that do not have project ( for example user creation, segment creation etc), will not be displayed to non root admins. --- .../features/access/access-read-model-type.ts | 3 + src/lib/features/access/access-read-model.ts | 28 ++++ .../features/access/createAccessReadModel.ts | 24 +++ .../features/access/createAccessService.ts | 1 - .../features/events/createEventsService.ts | 8 + .../events/event-created-by-migration.test.ts | 11 +- src/lib/features/events/event-service.ts | 15 ++ .../features/feature-search/search-utils.ts | 7 +- .../feature-toggle-strategies-store-type.ts | 2 +- src/lib/features/segment/segment-service.ts | 2 +- src/lib/services/access-service.test.ts | 19 ++- src/lib/services/access-service.ts | 12 -- .../feature-service-potentially-stale.test.ts | 1 + src/lib/types/events.ts | 4 +- .../e2e/api/admin/event-search.e2e.test.ts | 153 ++++++++++++++++-- 15 files changed, 246 insertions(+), 44 deletions(-) create mode 100644 src/lib/features/access/access-read-model-type.ts create mode 100644 src/lib/features/access/access-read-model.ts create mode 100644 src/lib/features/access/createAccessReadModel.ts diff --git a/src/lib/features/access/access-read-model-type.ts b/src/lib/features/access/access-read-model-type.ts new file mode 100644 index 000000000000..0516327bc267 --- /dev/null +++ b/src/lib/features/access/access-read-model-type.ts @@ -0,0 +1,3 @@ +export interface IAccessReadModel { + isRootAdmin(userId: number): Promise; +} diff --git a/src/lib/features/access/access-read-model.ts b/src/lib/features/access/access-read-model.ts new file mode 100644 index 000000000000..eb709950c53d --- /dev/null +++ b/src/lib/features/access/access-read-model.ts @@ -0,0 +1,28 @@ +import { + ADMIN_TOKEN_USER, + type IAccessStore, + type IUnleashStores, + SYSTEM_USER_ID, +} from '../../types'; +import type { IAccessReadModel } from './access-read-model-type'; +import * as permissions from '../../types/permissions'; + +const { ADMIN } = permissions; + +export class AccessReadModel implements IAccessReadModel { + private store: IAccessStore; + + constructor({ accessStore }: Pick) { + this.store = accessStore; + } + + async isRootAdmin(userId: number): Promise { + if (userId === SYSTEM_USER_ID || userId === ADMIN_TOKEN_USER.id) { + return true; + } + const roles = await this.store.getRolesForUserId(userId); + return roles.some( + (role) => role.name.toLowerCase() === ADMIN.toLowerCase(), + ); + } +} diff --git a/src/lib/features/access/createAccessReadModel.ts b/src/lib/features/access/createAccessReadModel.ts new file mode 100644 index 000000000000..f0fc23c6505d --- /dev/null +++ b/src/lib/features/access/createAccessReadModel.ts @@ -0,0 +1,24 @@ +import type { Db, IUnleashConfig } from '../../server-impl'; +import type { IAccessReadModel } from './access-read-model-type'; +import { AccessReadModel } from './access-read-model'; +import { AccessStore } from '../../db/access-store'; +import FakeRoleStore from '../../../test/fixtures/fake-role-store'; +import FakeAccessStore from '../../../test/fixtures/fake-access-store'; +import type { IAccessStore } from '../../types'; + +export const createAccessReadModel = ( + db: Db, + config: IUnleashConfig, +): IAccessReadModel => { + const { eventBus, getLogger } = config; + const accessStore = new AccessStore(db, eventBus, getLogger); + return new AccessReadModel({ accessStore }); +}; + +export const createFakeAccessReadModel = ( + accessStore?: IAccessStore, +): IAccessReadModel => { + const roleStore = new FakeRoleStore(); + const finalAccessStore = accessStore ?? new FakeAccessStore(roleStore); + return new AccessReadModel({ accessStore: finalAccessStore }); +}; diff --git a/src/lib/features/access/createAccessService.ts b/src/lib/features/access/createAccessService.ts index b04b37fa8b0f..26791fc82fac 100644 --- a/src/lib/features/access/createAccessService.ts +++ b/src/lib/features/access/createAccessService.ts @@ -33,7 +33,6 @@ export const createAccessService = ( { getLogger }, eventService, ); - return new AccessService( { accessStore, accountStore, roleStore, environmentStore }, { getLogger }, diff --git a/src/lib/features/events/createEventsService.ts b/src/lib/features/events/createEventsService.ts index 3cfd82c51932..40ac16a3c36b 100644 --- a/src/lib/features/events/createEventsService.ts +++ b/src/lib/features/events/createEventsService.ts @@ -13,6 +13,10 @@ import { createFakePrivateProjectChecker, createPrivateProjectChecker, } from '../private-project/createPrivateProjectChecker'; +import { + createAccessReadModel, + createFakeAccessReadModel, +} from '../access/createAccessReadModel'; export const createEventsService: ( db: Db, @@ -25,10 +29,12 @@ export const createEventsService: ( config.getLogger, ); const privateProjectChecker = createPrivateProjectChecker(db, config); + const accessReadModel = createAccessReadModel(db, config); return new EventService( { eventStore, featureTagStore }, config, privateProjectChecker, + accessReadModel, ); }; @@ -43,9 +49,11 @@ export const createFakeEventsService: ( const featureTagStore = stores?.featureTagStore || new FakeFeatureTagStore(); const fakePrivateProjectChecker = createFakePrivateProjectChecker(); + const fakeAccessReadModel = createFakeAccessReadModel(); return new EventService( { eventStore, featureTagStore }, config, fakePrivateProjectChecker, + fakeAccessReadModel, ); }; diff --git a/src/lib/features/events/event-created-by-migration.test.ts b/src/lib/features/events/event-created-by-migration.test.ts index bf33a74df839..113ad7debe60 100644 --- a/src/lib/features/events/event-created-by-migration.test.ts +++ b/src/lib/features/events/event-created-by-migration.test.ts @@ -5,7 +5,7 @@ import { EventEmitter } from 'stream'; import { EVENTS_CREATED_BY_PROCESSED } from '../../metric-events'; import type { IUnleashConfig } from '../../types'; import { createTestConfig } from '../../../test/config/test-config'; -import EventService from './event-service'; +import { createEventsService } from './createEventsService'; let db: ITestDb; @@ -126,14 +126,11 @@ test('emits events with details on amount of updated rows', async () => { const store = new EventStore(db.rawDatabase, getLogger); const eventBus = new EventEmitter(); - const service = new EventService( - { eventStore: store, featureTagStore: db.stores.featureTagStore }, - { getLogger, eventBus }, - {} as any, - ); + const config = createTestConfig(); + const service = createEventsService(db.rawDatabase, config); let triggered = false; - eventBus.on(EVENTS_CREATED_BY_PROCESSED, ({ updated }) => { + config.eventBus.on(EVENTS_CREATED_BY_PROCESSED, ({ updated }) => { expect(updated).toBe(2); triggered = true; }); diff --git a/src/lib/features/events/event-service.ts b/src/lib/features/events/event-service.ts index 919870ed35aa..72b5e09c9147 100644 --- a/src/lib/features/events/event-service.ts +++ b/src/lib/features/events/event-service.ts @@ -16,6 +16,7 @@ import { parseSearchOperatorValue } from '../feature-search/search-utils'; import { endOfDay, formatISO } from 'date-fns'; import type { IPrivateProjectChecker } from '../private-project/privateProjectCheckerType'; import type { ProjectAccess } from '../private-project/privateProjectStore'; +import type { IAccessReadModel } from '../access/access-read-model-type'; export default class EventService { private logger: Logger; @@ -24,6 +25,8 @@ export default class EventService { private featureTagStore: IFeatureTagStore; + private accessReadModel: IAccessReadModel; + private privateProjectChecker: IPrivateProjectChecker; private eventBus: EventEmitter; @@ -35,12 +38,14 @@ export default class EventService { }: Pick, { getLogger, eventBus }: Pick, privateProjectChecker: IPrivateProjectChecker, + accessReadModel: IAccessReadModel, ) { this.logger = getLogger('services/event-service.ts'); this.eventStore = eventStore; this.privateProjectChecker = privateProjectChecker; this.featureTagStore = featureTagStore; this.eventBus = eventBus; + this.accessReadModel = accessReadModel; } async getEvents(): Promise { @@ -77,6 +82,8 @@ export default class EventService { ); const queryParams = this.convertToDbParams(search); + const projectFilter = await this.getProjectFilterForNonAdmins(userId); + queryParams.push(...projectFilter); const totalEvents = await this.eventStore.searchEventsCount( { @@ -222,6 +229,14 @@ export default class EventService { async getEventCreators() { return this.eventStore.getEventCreators(); } + + async getProjectFilterForNonAdmins(userId: number): Promise { + const isRootAdmin = await this.accessReadModel.isRootAdmin(userId); + if (!isRootAdmin) { + return [{ field: 'project', operator: 'IS_NOT', values: [null] }]; + } + return []; + } } export const filterAccessibleProjects = ( diff --git a/src/lib/features/feature-search/search-utils.ts b/src/lib/features/feature-search/search-utils.ts index 026f4ee04da3..a9dbe49d0abc 100644 --- a/src/lib/features/feature-search/search-utils.ts +++ b/src/lib/features/feature-search/search-utils.ts @@ -50,6 +50,7 @@ export const applyGenericQueryParams = ( queryParams: IQueryParam[], ): void => { queryParams.forEach((param) => { + const isSingleParam = param.values.length === 1; switch (param.operator) { case 'IS': case 'IS_ANY_OF': @@ -57,7 +58,11 @@ export const applyGenericQueryParams = ( break; case 'IS_NOT': case 'IS_NONE_OF': - query.whereNotIn(param.field, param.values); + if (isSingleParam) { + query.whereNot(param.field, param.values[0]); + } else { + query.whereNotIn(param.field, param.values); + } break; case 'IS_BEFORE': query.where(param.field, '<', param.values[0]); diff --git a/src/lib/features/feature-toggle/types/feature-toggle-strategies-store-type.ts b/src/lib/features/feature-toggle/types/feature-toggle-strategies-store-type.ts index 4226de53b8d4..6e9cc96b295a 100644 --- a/src/lib/features/feature-toggle/types/feature-toggle-strategies-store-type.ts +++ b/src/lib/features/feature-toggle/types/feature-toggle-strategies-store-type.ts @@ -55,7 +55,7 @@ export type IQueryOperator = export interface IQueryParam { field: string; operator: IQueryOperator; - values: string[]; + values: (string | null)[]; } export interface IFeatureStrategiesStore diff --git a/src/lib/features/segment/segment-service.ts b/src/lib/features/segment/segment-service.ts index e1c83ed59b70..6ce561ca5deb 100644 --- a/src/lib/features/segment/segment-service.ts +++ b/src/lib/features/segment/segment-service.ts @@ -154,7 +154,7 @@ export class SegmentService implements ISegmentService { await this.eventService.storeEvent( new SegmentCreatedEvent({ data: segment, - project: segment.project || 'no-project', + project: segment.project, auditUser, }), ); diff --git a/src/lib/services/access-service.test.ts b/src/lib/services/access-service.test.ts index c7ff960f25f7..ac170a008695 100644 --- a/src/lib/services/access-service.test.ts +++ b/src/lib/services/access-service.test.ts @@ -23,13 +23,22 @@ import { } from '../../lib/types'; import BadDataError from '../../lib/error/bad-data-error'; import { createFakeEventsService } from '../../lib/features/events/createEventsService'; +import { createFakeAccessReadModel } from '../features/access/createAccessReadModel'; function getSetup() { const config = createTestConfig({ getLogger, }); - return createFakeAccessService(config); + const { accessService, eventStore, accessStore } = + createFakeAccessService(config); + + return { + accessService, + eventStore, + accessStore, + accessReadModel: createFakeAccessReadModel(accessStore), + }; } test('should fail when name exists', async () => { @@ -287,28 +296,28 @@ describe('addAccessToProject', () => { }); test('should return true if user has admin role', async () => { - const { accessService, accessStore } = getSetup(); + const { accessReadModel, accessStore } = getSetup(); const userId = 1; accessStore.getRolesForUserId = jest .fn() .mockResolvedValue([{ id: 1, name: 'ADMIN', type: 'custom' }]); - const result = await accessService.isRootAdmin(userId); + const result = await accessReadModel.isRootAdmin(userId); expect(result).toBe(true); expect(accessStore.getRolesForUserId).toHaveBeenCalledWith(userId); }); test('should return false if user does not have admin role', async () => { - const { accessService, accessStore } = getSetup(); + const { accessReadModel, accessStore } = getSetup(); const userId = 2; accessStore.getRolesForUserId = jest .fn() .mockResolvedValue([{ id: 2, name: 'user', type: 'custom' }]); - const result = await accessService.isRootAdmin(userId); + const result = await accessReadModel.isRootAdmin(userId); expect(result).toBe(false); expect(accessStore.getRolesForUserId).toHaveBeenCalledWith(userId); diff --git a/src/lib/services/access-service.ts b/src/lib/services/access-service.ts index 9ab3978eeab7..9842085b7807 100644 --- a/src/lib/services/access-service.ts +++ b/src/lib/services/access-service.ts @@ -41,13 +41,11 @@ import BadDataError from '../error/bad-data-error'; import type { IGroup } from '../types/group'; import type { GroupService } from './group-service'; import { - ADMIN_TOKEN_USER, type IUnleashConfig, type IUserAccessOverview, RoleCreatedEvent, RoleDeletedEvent, RoleUpdatedEvent, - SYSTEM_USER_ID, } from '../types'; import type EventService from '../features/events/event-service'; @@ -889,14 +887,4 @@ export class AccessService { async getUserAccessOverview(): Promise { return this.store.getUserAccessOverview(); } - - async isRootAdmin(userId: number): Promise { - if (userId === SYSTEM_USER_ID || userId === ADMIN_TOKEN_USER.id) { - return true; - } - const roles = await this.store.getRolesForUserId(userId); - return roles.some( - (role) => role.name.toLowerCase() === ADMIN.toLowerCase(), - ); - } } diff --git a/src/lib/services/feature-service-potentially-stale.test.ts b/src/lib/services/feature-service-potentially-stale.test.ts index afc30c95a931..c42f64c6ef29 100644 --- a/src/lib/services/feature-service-potentially-stale.test.ts +++ b/src/lib/services/feature-service-potentially-stale.test.ts @@ -44,6 +44,7 @@ test('Should only store events for potentially stale on', async () => { }, config, {}, + {}, ); const featureToggleService = new FeatureToggleService( diff --git a/src/lib/types/events.ts b/src/lib/types/events.ts index f23db6c57e6c..26db0b636c26 100644 --- a/src/lib/types/events.ts +++ b/src/lib/types/events.ts @@ -1930,12 +1930,12 @@ export class AddonConfigDeletedEvent extends BaseEvent { } export class SegmentCreatedEvent extends BaseEvent { - readonly project: string; + readonly project: string | undefined; readonly data: any; constructor(eventData: { auditUser: IAuditUser; - project: string; + project: string | undefined; data: any; }) { super(SEGMENT_CREATED, eventData.auditUser); diff --git a/src/test/e2e/api/admin/event-search.e2e.test.ts b/src/test/e2e/api/admin/event-search.e2e.test.ts index c6c2abbf1022..5281290acdfb 100644 --- a/src/test/e2e/api/admin/event-search.e2e.test.ts +++ b/src/test/e2e/api/admin/event-search.e2e.test.ts @@ -1,34 +1,97 @@ import type { EventSearchQueryParameters } from '../../../../lib/openapi/spec/event-search-query-parameters'; import dbInit, { type ITestDb } from '../../helpers/database-init'; -import { FEATURE_CREATED, type IUnleashConfig } from '../../../../lib/types'; -import type { EventService } from '../../../../lib/services'; -import getLogger from '../../../fixtures/no-logger'; import { - type IUnleashTest, - setupAppWithCustomConfig, -} from '../../helpers/test-helper'; + FEATURE_CREATED, + type IUnleashConfig, + type IUnleashStores, + RoleName, + USER_CREATED, +} from '../../../../lib/types'; +import type { AccessService, EventService } from '../../../../lib/services'; +import getLogger from '../../../fixtures/no-logger'; +import { type IUnleashTest, setupAppWithAuth } from '../../helpers/test-helper'; import { createEventsService } from '../../../../lib/features'; import { createTestConfig } from '../../../config/test-config'; +import type { IRole } from '../../../../lib/types/stores/access-store'; let app: IUnleashTest; let db: ITestDb; let eventService: EventService; const TEST_USER_ID = -9999; +const regularUserName = 'import-user'; +const adminUserName = 'admin-user'; const config: IUnleashConfig = createTestConfig(); +let adminRole: IRole; +let stores: IUnleashStores; +let accessService: AccessService; + +const loginRegularUser = () => + app.request + .post(`/auth/demo/login`) + .send({ + email: `${regularUserName}@getunleash.io`, + }) + .expect(200); + +const loginAdminUser = () => + app.request + .post(`/auth/demo/login`) + .send({ + email: `${adminUserName}@getunleash.io`, + }) + .expect(200); + +const createUserEditorAccess = async (name, email) => { + const { userStore } = stores; + const user = await userStore.insert({ + name, + email, + }); + return user; +}; + +const createUserAdminAccess = async (name, email) => { + const { userStore } = stores; + const user = await userStore.insert({ + name, + email, + }); + await accessService.addUserToRole(user.id, adminRole.id, 'default'); + return user; +}; beforeAll(async () => { db = await dbInit('event_search', getLogger); - app = await setupAppWithCustomConfig(db.stores, { - experimental: { - flags: { - strictSchemaValidation: true, + stores = db.stores; + app = await setupAppWithAuth( + db.stores, + { + experimental: { + flags: { + strictSchemaValidation: true, + }, }, }, - }); + db.rawDatabase, + ); eventService = createEventsService(db.rawDatabase, config); + + accessService = app.services.accessService; + + const roles = await accessService.getRootRoles(); + adminRole = roles.find((role) => role.name === RoleName.ADMIN)!; + + await createUserEditorAccess( + regularUserName, + `${regularUserName}@getunleash.io`, + ); + await createUserAdminAccess( + adminUserName, + `${adminUserName}@getunleash.io`, + ); }); afterAll(async () => { @@ -37,6 +100,7 @@ afterAll(async () => { }); beforeEach(async () => { + await loginAdminUser(); await db.stores.featureToggleStore.deleteAll(); await db.stores.segmentStore.deleteAll(); await db.stores.eventStore.deleteAll(); @@ -182,6 +246,7 @@ test('should filter events by type', async () => { test('should filter events by created by', async () => { await eventService.storeEvent({ type: FEATURE_CREATED, + project: 'default', createdBy: 'admin1@example.com', createdByUserId: TEST_USER_ID + 1, ip: '127.0.0.1', @@ -189,6 +254,7 @@ test('should filter events by created by', async () => { await eventService.storeEvent({ type: FEATURE_CREATED, + project: 'default', createdBy: 'admin2@example.com', createdByUserId: TEST_USER_ID, ip: '127.0.0.1', @@ -311,8 +377,7 @@ test('should filter events by feature using IS_ANY_OF', async () => { }); await eventService.storeEvent({ - type: FEATURE_CREATED, - project: 'default', + type: USER_CREATED, featureName: 'my_feature_b', createdBy: 'test-user', createdByUserId: TEST_USER_ID, @@ -335,7 +400,7 @@ test('should filter events by feature using IS_ANY_OF', async () => { expect(body).toMatchObject({ events: [ { - type: 'feature-created', + type: 'user-created', featureName: 'my_feature_b', }, { @@ -390,3 +455,63 @@ test('should filter events by project using IS_ANY_OF', async () => { total: 2, }); }); + +test('should not show user creation events for non-admins', async () => { + await loginRegularUser(); + await eventService.storeEvent({ + type: USER_CREATED, + createdBy: 'test-user', + createdByUserId: TEST_USER_ID, + ip: '127.0.0.1', + }); + + await eventService.storeEvent({ + type: FEATURE_CREATED, + project: 'default', + createdBy: 'test-user', + createdByUserId: TEST_USER_ID, + ip: '127.0.0.1', + }); + + const { body } = await searchEvents({}); + + expect(body).toMatchObject({ + events: [ + { + type: FEATURE_CREATED, + }, + ], + total: 1, + }); +}); + +test('should show user creation events for admins', async () => { + await eventService.storeEvent({ + type: USER_CREATED, + createdBy: 'test-user', + createdByUserId: TEST_USER_ID, + ip: '127.0.0.1', + }); + + await eventService.storeEvent({ + type: FEATURE_CREATED, + project: 'default', + createdBy: 'test-user', + createdByUserId: TEST_USER_ID, + ip: '127.0.0.1', + }); + + const { body } = await searchEvents({}); + + expect(body).toMatchObject({ + events: [ + { + type: FEATURE_CREATED, + }, + { + type: USER_CREATED, + }, + ], + total: 2, + }); +}); From 45de8ceae0e28f00a88375064e5b3208d06924a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Wed, 21 Aug 2024 13:01:00 +0200 Subject: [PATCH 11/13] chore: type our path parameters when they are numbers (#4471) Path types in our openapi are inferred as string (which is a sensible default). But we can be more specific and provide the right type for each parameter. This is one example of how we can do that --- src/lib/routes/admin-api/user-admin.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/lib/routes/admin-api/user-admin.ts b/src/lib/routes/admin-api/user-admin.ts index 0f9e2448cf3e..ac0025d15837 100644 --- a/src/lib/routes/admin-api/user-admin.ts +++ b/src/lib/routes/admin-api/user-admin.ts @@ -363,6 +363,15 @@ export default class UserAdminController extends Controller { description: 'Only the explicitly specified fields get updated.', requestBody: createRequestSchema('updateUserSchema'), + parameters: [ + { + name: 'id', + in: 'path', + schema: { + type: 'integer', + }, + }, + ], responses: { 200: createResponseSchema('createUserResponseSchema'), ...getStandardResponses(400, 401, 403, 404), From 2a13139215d6a1b952bcb3f4cd7618b0a92d0bc8 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Wed, 21 Aug 2024 13:12:42 +0200 Subject: [PATCH 12/13] fix: project owner name overflow (#7949) --- .../project/NewProjectCard/ProjectOwners/ProjectOwners.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/component/project/NewProjectCard/ProjectOwners/ProjectOwners.tsx b/frontend/src/component/project/NewProjectCard/ProjectOwners/ProjectOwners.tsx index 4082de18fba2..3f9d22864b8e 100644 --- a/frontend/src/component/project/NewProjectCard/ProjectOwners/ProjectOwners.tsx +++ b/frontend/src/component/project/NewProjectCard/ProjectOwners/ProjectOwners.tsx @@ -63,6 +63,7 @@ const StyledWrapper = styled('div')(({ theme }) => ({ padding: theme.spacing(1.5, 0, 2.5, 3), display: 'flex', alignItems: 'center', + minWidth: 0, })); export const ProjectOwners: FC = ({ owners = [] }) => { From 48423fa980e971789912b9c63816467802f0eeab Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Wed, 21 Aug 2024 13:17:33 +0200 Subject: [PATCH 13/13] fix: enable disabled strategies keeps settings (#7950) --- .../feature-toggle/feature-toggle-service.ts | 2 +- .../tests/feature-toggle-service.e2e.test.ts | 48 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/lib/features/feature-toggle/feature-toggle-service.ts b/src/lib/features/feature-toggle/feature-toggle-service.ts index af76a4dac7ed..dc6d4988ba0a 100644 --- a/src/lib/features/feature-toggle/feature-toggle-service.ts +++ b/src/lib/features/feature-toggle/feature-toggle-service.ts @@ -1896,7 +1896,7 @@ class FeatureToggleService { strategies.map((strategy) => this.updateStrategy( strategy.id, - { disabled: false }, + { ...strategy, disabled: false }, { environment, projectId: project, diff --git a/src/lib/features/feature-toggle/tests/feature-toggle-service.e2e.test.ts b/src/lib/features/feature-toggle/tests/feature-toggle-service.e2e.test.ts index 6662fe568cd3..5fd0b835b2a0 100644 --- a/src/lib/features/feature-toggle/tests/feature-toggle-service.e2e.test.ts +++ b/src/lib/features/feature-toggle/tests/feature-toggle-service.e2e.test.ts @@ -814,3 +814,51 @@ test('Should not allow to revive flags to archived projects', async () => { ), ); }); + +test('Should enable disabled strategies on feature environment enabled', async () => { + const flagName = 'enableThisFlag'; + const project = 'default'; + const environment = 'default'; + const shouldActivateDisabledStrategies = true; + await service.createFeatureToggle( + project, + { + name: flagName, + }, + TEST_AUDIT_USER, + ); + const config: Omit = { + name: 'default', + constraints: [ + { contextName: 'userId', operator: 'IN', values: ['1', '1'] }, + ], + parameters: { param: 'a' }, + variants: [ + { + name: 'a', + weight: 100, + weightType: 'variable', + stickiness: 'random', + }, + ], + disabled: true, + }; + const createdConfig = await service.createStrategy( + config, + { projectId: project, featureName: flagName, environment }, + TEST_AUDIT_USER, + ); + + await service.updateEnabled( + project, + flagName, + environment, + true, + TEST_AUDIT_USER, + { email: 'test@example.com' } as User, + shouldActivateDisabledStrategies, + ); + + const strategy = await service.getStrategy(createdConfig.id); + expect(strategy).toMatchObject({ ...config, disabled: false }); +});