From aa1ed0bc09a16b7ad1f6a9e1dc26cac81fb4f794 Mon Sep 17 00:00:00 2001 From: lena sano Date: Wed, 26 Feb 2025 12:24:03 -0300 Subject: [PATCH 01/19] FME docs initial commit (HDH landing page tile and Documentation menu) + Getting started section --- .github/CODEOWNERS | 13 +- docs/feature-management-experimentation.md | 14 + .../10-getting-started/docs/_category_.json | 7 + .../10-getting-started/docs/key-concepts.md | 104 +++++ .../docs/onboarding-guide.md | 20 + .../10-getting-started/docs/overview.md | 49 ++ .../docs/split-and-harness.md | 28 ++ .../docs/static/fme-quickstart.png | Bin 0 -> 538736 bytes .../docs/static/overview.png | Bin 0 -> 340393 bytes .../10-getting-started/whats-supported.md | 154 +++++++ .../_category_.json | 10 + .../fme-support.md | 61 +++ .../shared/OutboundLink.mdx | 1 + .../shared/_find-sdk-api-key.mdx | 1 + docusaurus.config.ts | 4 + sidebars.ts | 115 ++++- .../Docs/FeatureManagementExperimentation.tsx | 46 ++ src/components/Docs/IncidentResponse.tsx | 4 +- .../featureManagementExperimentationData.ts | 77 ++++ .../HomepageFeatures/data/featureListData.tsx | 7 + .../HomepageFeatures/styles.module.scss | 3 + .../Roadmap/HorizonCard/styles.module.scss | 6 + src/components/Roadmap/data/cdData.ts | 12 +- .../TutorialCard/TutorialCard.module.scss | 3 + src/constants.ts | 2 +- src/css/custom.css | 16 + static/img/fme-docs-main-dark-mode.svg | 424 ++++++++++++++++++ static/img/fme-docs-main-light-mode.svg | 416 +++++++++++++++++ 28 files changed, 1580 insertions(+), 17 deletions(-) create mode 100644 docs/feature-management-experimentation.md create mode 100644 docs/feature-management-experimentation/10-getting-started/docs/_category_.json create mode 100644 docs/feature-management-experimentation/10-getting-started/docs/key-concepts.md create mode 100644 docs/feature-management-experimentation/10-getting-started/docs/onboarding-guide.md create mode 100644 docs/feature-management-experimentation/10-getting-started/docs/overview.md create mode 100644 docs/feature-management-experimentation/10-getting-started/docs/split-and-harness.md create mode 100644 docs/feature-management-experimentation/10-getting-started/docs/static/fme-quickstart.png create mode 100644 docs/feature-management-experimentation/10-getting-started/docs/static/overview.png create mode 100644 docs/feature-management-experimentation/10-getting-started/whats-supported.md create mode 100644 docs/feature-management-experimentation/_category_.json create mode 100644 docs/feature-management-experimentation/fme-support.md create mode 100644 docs/feature-management-experimentation/shared/OutboundLink.mdx create mode 100644 docs/feature-management-experimentation/shared/_find-sdk-api-key.mdx create mode 100755 src/components/Docs/FeatureManagementExperimentation.tsx create mode 100644 src/components/Docs/data/featureManagementExperimentationData.ts create mode 100644 static/img/fme-docs-main-dark-mode.svg create mode 100644 static/img/fme-docs-main-light-mode.svg diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0c7ab15d0e9..46c4da11eb6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -105,13 +105,11 @@ static/ @rohanmaharjan100 @wei-harness /src/components/Docs/data/serviceReliabilityManagementData.ts @sunilgupta-harness # Chaos Docs -/docs/chaos-engineering/ @neelanjan00 @SmritiSatya @shovanmaity @Jonsy13 @Saranya-jena @Adarshkumar14 @ispeakc0de @amityt @uditgaurav @S-ayanide @vanshBhatia-A4k9 @ksatchit @umamukkara @SarthakJain26 +/docs/chaos-engineering/ @neelanjan00 @SmritiSatya @shovanmaity @Jonsy13 @Saranya-jena @Adarshkumar14 @ispeakc0de @amityt @uditgaurav @S-ayanide @vanshBhatia-A4k9 @ksatchit @umamukkara /release-notes/chaos-engineering.md @neelanjan00 @SmritiSatya @Jonsy13 /src/components/Roadmap/data/ceData.ts @krishi0408 @neelanjan00 @SmritiSatya @vishal-av @SushrutHarness /src/components/Docs/data/chaosEngineeringData* @krishi0408 @neelanjan00 @SmritiSatya @vishal-av @SushrutHarness # Chaos FAQs -/docs/faqs/chaos-engineering-faqs @neelanjan00 @SmritiSatya @shovanmaity @Jonsy13 @Saranya-jena @Adarshkumar14 @ispeakc0de @amityt @uditgaurav @S-ayanide @vanshBhatia-A4k9 @ksatchit @umamukkara @SarthakJain26 -/docs/faqs/static @neelanjan00 @SmritiSatya @Jonsy13 @Adarshkumar14 @ksatchit @umamukkara /kb/chaos-engineering/chaos-engineering-faq @neelanjan00 @SmritiSatya @ksatchit @umamukkara # Platform Docs @@ -141,10 +139,10 @@ static/ @rohanmaharjan100 @wei-harness /docs/platform/connectors/ @krishi0408 @vishal-av # IDP Docs -/docs/internal-developer-portal/ @Debanitrkl @OrkoHunter @manishas @khushisharmaharness -/release-notes/internal-developer-portal.md @Debanitrkl @OrkoHunter @manishas @khushisharmaharness -/src/components/Roadmap/data/idpData.ts @Debanitrkl @OrkoHunter @manishas @khushisharmaharness -/src/components/Docs/data/internalDeveloperPortal.ts @Debanitrkl @OrkoHunter @manishas @khushisharmaharness +/docs/internal-developer-portal/ @Debanitrkl @OrkoHunter @manishas +/release-notes/internal-developer-portal.md @Debanitrkl @OrkoHunter @manishas +/src/components/Roadmap/data/idpData.ts @Debanitrkl @OrkoHunter @manishas +/src/components/Docs/data/internalDeveloperPortal.ts @Debanitrkl @OrkoHunter @manishas # SCS Docs /docs/software-supply-chain-assurance/ @sunilgupta-harness @tejakummarikuntla @pranay-harness @@ -195,6 +193,7 @@ static/ @rohanmaharjan100 @wei-harness #FME Docs /src/components/Roadmap/data/fmeData.ts @dtk-DaveKarow @kleinjoshuaa @deej-split +/docs/feature-management-experimentation/ @lenasano # Open Source Docs /docs/open-source/ @dewan-ahmed @rustd @SushrutHarness diff --git a/docs/feature-management-experimentation.md b/docs/feature-management-experimentation.md new file mode 100644 index 00000000000..7115342c22e --- /dev/null +++ b/docs/feature-management-experimentation.md @@ -0,0 +1,14 @@ +--- +hide_table_of_contents: true +hide_title: true +title: Feature Management & Experimentation Documentation +id: feature-management-experimentation +--- + + + + + +import FeatureManagementExperimentation from '@site/src/components/Docs/FeatureManagementExperimentation'; + + diff --git a/docs/feature-management-experimentation/10-getting-started/docs/_category_.json b/docs/feature-management-experimentation/10-getting-started/docs/_category_.json new file mode 100644 index 00000000000..13330ee79ae --- /dev/null +++ b/docs/feature-management-experimentation/10-getting-started/docs/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Getting started", + "collapsible": true, + "collapsed": true, + "className": "red", + "position": 3 +} \ No newline at end of file diff --git a/docs/feature-management-experimentation/10-getting-started/docs/key-concepts.md b/docs/feature-management-experimentation/10-getting-started/docs/key-concepts.md new file mode 100644 index 00000000000..34133130962 --- /dev/null +++ b/docs/feature-management-experimentation/10-getting-started/docs/key-concepts.md @@ -0,0 +1,104 @@ +--- +title: Key concepts +sidebar_label: Key concepts +sidebar_position: 3 +helpdocs_is_private: false +helpdocs_is_published: true +--- + +## Key concepts +Take 5 minutes to learn the foundational concepts of Split’s Feature Data Platform. + +## What is a feature flag? +A feature flag wraps or gates a section of your code, allowing it to be selectively turned on or off remotely with precision, down to the level of an individual user, at any time, without a new code deployment. + +### Decouple your deploy from your feature release +Feature flags allow you to decouple your deploy from your release, so your work in progress and new features are deployed in a turned-off state to any environment, which includes production, without impacting your users. + +### Control your release with targeting rules +Once your code is deployed, you can instantly turn on or off features for any individual user, group of users, or percentage of users, by creating or updating targeting rules. This approach facilitates faster software delivery practices with greater safety, including: + +Trunk-based development to reduce time lost merging code branches +Testing in production to allow dev, QA, and stakeholder review without impacting your users +Early access or beta testing for a subset of your users in production +Canary releases and monitored rollouts to limit the blast radius of release incidents +Instant kill switches to shut off exposure to a feature without rollback or redeploy +Infrastructure migration without downtime or risk of data loss +Experimentation and A/B testing to make bigger bets with less risk + +## The role of data in Split +The Split Feature Data Platform provides visibility into your controlled releases by comparing data about feature flag evaluations with data about what happened after those evaluations. The data points that feed those comparisons are impressions and events. The results of those comparisons are called metrics. + +### Impressions +An impression is a record of a targeting decision made. It is created automatically each time a feature flag is evaluated and contains details about the user or unique key for which the evaluation was performed, the targeting decision, the targeting rule that drove that decision, and a time stamp. Refer to the Impressions guide for more information. + +### Events +An event is a record of user or system behavior. Events can be as simple as a page visited, a button clicked, or response time observed, and as complex as a transaction record with a detailed list of properties. An event doesn’t refer to a feature flag. The association between flag evaluations and events is computed for you. An event, associated with a user (or other unique keys), arriving after a flag decision for that same unique key, is attributed to that evaluation by Split’s attribution engine. + +To be ingested by Split, an event must contain the same user or unique key for which a feature flag evaluation was performed and a time stamp. Events are sent to Split from within your application, either from an existing customer data platform or error subsystem, or with a bulk upload using Split’s REST API. Numerous events in integrations streamline event ingest for you. + +### Metrics +Split calculates metrics by attributing events to impressions and applying metric definitions to them. A metric definition can be as simple as a count of events per user or as complex as an average of values pulled from an event’s property after filtering those same events by another property. + +For example, from a stream of room_reservation events, calculate the average number of room nights booked for platinum members by examining the room_nights property after filtering the room_reservation events to those where the property club_membership = platinum. + +To promote one version of the truth, metrics are defined in a central location, not on a flag-by-flag basis, and all metrics are calculated for all flags. Split lets you elevate any metric your account created to be a key metric for a given feature flag. Then all the remaining metrics are sorted by impact and displayed immediately below the key metrics. This design, unique to Split, avoids blind spots caused by only looking for what you expect to find which automatically surfaces unexpected impacts. Refer to the Metrics guide for more information. + +### Alerts +Alerts notify metric stakeholders and the team rolling out a particular feature when a metric threshold has exceeded a rollout or experiment that uses a percentage rollout rule. + +Alerts, like the metrics they are based on, are centrally defined once, and then applied to every rollout or experiment automatically. This is another design unique to Split. Our goal is to make learning and safety at speed the default experience, for every rollout. Once you define thresholds for metrics, any future rollout or experiment that exceeds them will fire an alert. When that happens, notifications are sent out, and an alert box is presented on the Targeting and Alerts tabs for the feature flag in question. Refer to the Configuring metric alerting guide for more information. + +Using Split in your application +Targeting decisions are made locally, in memory, from within your own application code. There is never a reason to send private user data to Split’s network. Let’s take a look at how this is accomplished. + +Split SDKs +To use Split, include and initialize one of Split’s SDKs in your application. Once the SDK is initialized, targeting rules are retrieved from a nearby content delivery network (CDN) node, cached inside your code, and updated in real-time in milliseconds using a streaming architecture. + +As needed, your application makes a just-in-time call to the Split SDK in local memory, passing the feature flag name, the userId or unique key, and optionally, a map of user or session attributes. The response is returned instantly, with no need for a network call. After the evaluation is performed, the SDK asynchronously returns an impression record to Split. Refer to our SDK overview for more information. + +Split evaluator +As an alternative to using Split’s SDKs, you can make REST API calls to a Split Evaluator hosted inside your own infrastructure. Like the SDK, this method never requires you to send private user data to Split’s network. The evaluator makes it possible to operate from within languages that do not yet have a published Split SDK and should only be used in that case. Refer to the Split evaluator guide for more information. + +Split's structure +Split is architected to support teams and organizations of any size, from a single developer to multiple value-stream enterprises. Take a moment to familiarize yourself with the concepts of your Split account, project, environment, and objects, e.g., users, groups, tags, traffic types, feature flags, segments, and metrics. + +![](https://help.split.io/hc/article_attachments/30794709286029) + +Account +Your company has one Split account. Your account is the highest level container. Split support may ask you for your account ID to speed troubleshooting. You’ll find your account ID in the URL for every page you visit in the Split application. + +Users +A Split user is someone with access to the Split user interface. Administrators can invite new users to Split. All paid plans include SSO for user authentication and can support either invites or just in time provisioning. + +Groups +A group is a convenient way to manage a collection of users in your account. You can use groups to grant administrative controls and grant environment, feature flag, or segment-level controls. Refer to the Manage user groups guide for more information. + +Projects +Projects provide separation or partitioning of work to reduce clutter or to enforce security. All accounts have at least one project. Use multiple projects only when you want to deliberately separate the work of different teams, product lines, or areas of work from each other. By design, objects within Split are not meant to be shared or moved across projects. Refer to the Projects guide for more information. + +Environment +Within each project, you may have multiple environments, such as development, staging, and production. Refer to the Environments guide for more information. + +Feature Flags +Feature flags are created at the project level where you specify the feature flag name, traffic type, owners, and description. Targeting rules are then created and managed at the environment level as part of the feature flag definition. Refer to the Feature flag management guide for more information. + +Targeting rule +Targeting rules for each feature flag are created at the environment level. For example, this supports one set of rules in your staging environment and another in production. Rules may be based on user or device attributes, membership in a segment, a percentage of a randomly distributed population, a list of individually specified user or unique key targets, or any combination of the above. Refer to the Creating a rollout plan guide for more information. + +Segment +A segment is a list of users or unique keys for targeting purposes. Segments are created at the environment level. Refer to the Segments guide for more information. + +Traffic type +Targeting decisions are made on a per-user or per unique key basis, but what are the available types of unique keys you intend to target? These are your traffic types, and you can define up to ten unique key types at the project level. + +For feature flags that make decisions or observe metrics at the userId level, the traffic type should be user. If decisions and observations are based on account membership (to facilitate all users for a particular customer being treated the same, for instance), the traffic type should be account. Other common types are anonymous and device, but you have total flexibility in employing different traffic types. Refer to the Traffic type guide for more information. + +Tag +Use tags to organize and filter feature flags, segments, and metrics across the Split user interface. Because they allow you to filter items in lists, they are a great way to filter by team, epic, layer of system (front-end vs back-end), or any other. Refer to the Tags guide for more information on how to use them. + +Statuses +Statuses provide a way for teams to indicate which stage of a release or rollout a feature is in at any given moment, and as a way for teammates to filter their feature flags to see only features in a particular stage of the internal release process. There is a fixed list of status types. Refer to the Use statuses guide for more information. + +Additional essential guides +Now that you have a grounding in our foundational concepts, the following links take you to our essential guides that walk you through: \ No newline at end of file diff --git a/docs/feature-management-experimentation/10-getting-started/docs/onboarding-guide.md b/docs/feature-management-experimentation/10-getting-started/docs/onboarding-guide.md new file mode 100644 index 00000000000..60376d748ed --- /dev/null +++ b/docs/feature-management-experimentation/10-getting-started/docs/onboarding-guide.md @@ -0,0 +1,20 @@ +--- +title: Onboarding guide +description: Quickstarts for Split Feature Management & Experimentation +sidebar_label: Onboarding guide +sidebar_position: 2 +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +Quickstarts can help you get started with Split Feature Management & Experimentation. + +## Quickstart wizard + +You can follow the SDK Setup and Event Ingestion **Quickstart wizard** in Harness. Click **Help** in the left navigation menu, and click on one of the quickstarts. + +![A screenshot of the Quickstart guides in Split UI](./static/fme-quickstart.png) \ No newline at end of file diff --git a/docs/feature-management-experimentation/10-getting-started/docs/overview.md b/docs/feature-management-experimentation/10-getting-started/docs/overview.md new file mode 100644 index 00000000000..410b5e404d7 --- /dev/null +++ b/docs/feature-management-experimentation/10-getting-started/docs/overview.md @@ -0,0 +1,49 @@ +--- +title: Overview +sidebar_label: Overview +description: How to make Feature Management & Experimentation work for you +sidebar_position: 1 +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +Harness Feature Management & Experimentation (FME) combines capabilities for feature delivery and control with built-in tools for measurement and learning. FME connects insightful data to every feature release, eliminates hesitation from the software development process, and supports modern practices like continuous delivery and progressive delivery. + +![](./static/overview.png) + +### Architected for performance, security, and resilience + +Harness FME is built on a global feature flag and data processing architecture that serves 50 billion daily feature flags to over 2 billion end users around the globe. + +* **Performance:** FME streaming architecture pushes changes to its SDKs in milliseconds. +* **Security:** The SDKs evaluate feature flags locally, so customer data is never sent over the internet. +* **Resilience:** Our SaaS app, data platform, and API span multiple data centers. Plus, our SDKs cache locally to handle any network interruptions. + +Our stateless architecture scales to millions of users with no degradation in performance. FME SDKs reside in your frontend, backend, and mobile apps where they make feature flags and targeting decisions locally, without the need to send private user data outside your app for evaluation. + +## FME features + +### Feature flags +Feature flags turn on and off features to specific users or segments. You can tailor access to beta testers and early adopters based on individual IDs, attributes, dependencies, or percentages. Gradually target users little by little to limit the blast radius of your releases. + +### Release monitoring +Release monitoring detects the impact of each feature on system performance and user behavior, starting with the earliest stage of a gradual rollout. With detection and triage done at the flag level, you can ship more often and with greater confidence. + +### Experimentation +Experimentation centralizes notifications for metric impacts, review periods, and change requests, empowering your team with actionable data to make rapid, precise, data-driven decisions. + +## Harness platform + +If you're new to Harness, review the [Harness platform onboarding guide](/docs/platform/get-started/onboarding-guide) and [Harness platform key concepts](/docs/platform/get-started/key-concepts). + +## Get started with FME + +Some resources to get started with FME: + +* [What's supported?](./../whats-supported.md) +* [Onboarding guide](./onboarding-guide.md) +* [Key concepts](./key-concepts/) \ No newline at end of file diff --git a/docs/feature-management-experimentation/10-getting-started/docs/split-and-harness.md b/docs/feature-management-experimentation/10-getting-started/docs/split-and-harness.md new file mode 100644 index 00000000000..064a3d54dd6 --- /dev/null +++ b/docs/feature-management-experimentation/10-getting-started/docs/split-and-harness.md @@ -0,0 +1,28 @@ +--- +title: Split and Harness +description: Split is now Harness FME +sidebar_position: 18 +--- + +In June 2024, Harness acquired Split, now called Harness Feature Management & Experimentation (FME). This page provides information to support you during the transition to Feature Management & Experimentation in the Harness platform. + +If you are currently accessing Split via app.split.io, our customer success and support teams will be in contact with you to help you ensure a smooth transition into FME on app.harness.io. + +For more information about the acquisition, go to the [Harness blog](https://www.harness.io/blog/harness-to-acquire-split). + +## Get started with Harness + +If you're new to Harness, go to [Get started with Harness](/docs/category/get-started-with-harness) for a jumpstart into the Harness software delivery platform. + +## Authentication, access, and user management + +Authentication, access, and user management are part of the Harness platform. Permissions granted to users and user groups depends on their associations with resources and resource groups, which are controlled at the account, organization, and project level in Harness. For more information about authentication, access, and user management, go to the following: + +* [Harness platform authentication (including 2FA and SSO)](/docs/category/authentication) +* [Harness RBAC overview](/docs/platform/role-based-access-control/rbac-in-harness) + + + +## Harness platform integrations coming soon + +We are moving rapidly to unlock integrations with Harness's innovative DevOps tools, DevEx improvements, Security features, and Cloud optimizations, while also accelerating the roadmap of enhancements to the core of what you have previously known as Split. Please visit our [roadmap](https://developer.harness.io/roadmap/#fme) to learn more. \ No newline at end of file diff --git a/docs/feature-management-experimentation/10-getting-started/docs/static/fme-quickstart.png b/docs/feature-management-experimentation/10-getting-started/docs/static/fme-quickstart.png new file mode 100644 index 0000000000000000000000000000000000000000..123e48f51b51442f0b54bbcb052fb4b9241e5c07 GIT binary patch literal 538736 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%0Dt|P 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 z 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 vWO-i|RM= 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

La&IhWkj4CBD@ok#r{Rd+> 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νi;EE;VWrsd_$u}1#`#F$gghmbFv;3s*cm& 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 DElKXF literal 0 HcmV?d00001 diff --git a/docs/feature-management-experimentation/10-getting-started/docs/static/overview.png b/docs/feature-management-experimentation/10-getting-started/docs/static/overview.png new file mode 100644 index 0000000000000000000000000000000000000000..81b169877a6afe1785f8c0cf307532b59905a7e6 GIT binary patch literal 340393 zcmY&<19W9e(`Yo4iH(Vq6K7(3V%yflwrzW2n}eM;(y=2wf~bt=7RrUj=2#3OAW=I3;Dk=_}^B7fzhPtjQ!1E?8G%3!NB0q{&~T{ z(lfFDa?>zZR(DdDk>)b8wWc%pWou|m=Vop9Pc1MWH?F^+wXu@{k(;%ZjU$&EFUh|Y zTz}zz(DWok|B^Ua@{*{_$P)?KIv5kN(J|05knq705fSk?{4(KE6c+tY^}i)v5;G?! zJ1%;9S65d$S7tg}2UB`RPEJmG1}1tYCfdIgw2tmJP6lqYHjaS*Ve-FxgpD1I9L(*U z%x!Im{^4t2XzT36OG5IGp#P@-X{VF9$^VsP%n;L;v63#s~XdcNZEA>?fFn zuz<1~_<841`T<8>u>?{w3YDa>N(CU>-Q;O{V?uX+>m++~)v>MJsR)sG`!((Rx49D% zlKQqb&&+sI_+kUFv>;CLYT3`%PiOvvCe12XJgEW*!e2WFZlsTaoDq~_B2rJrfVy5Xs5>2N=_f4lm&~_-~LJATS4V-MJ(Z^w5>;cg;LIW!TLueK2RKlncxJ6?d@@M^YO@UF|t{Z&sH1}({mkFT%0 zAD1@Rng?x|t0fN8F(K1)(cPdHCNCDe-MiUZYr?E-c(Ia_eo3E9zE14yt~UqJsop0G zkLRWv zR7`K3*3IqLl*Y4yhmVrA+k!u9W0-6-<5N1N&a(vK=Hd>&y_Y_kZXMoZ|2*==)%|Gp zC7j~-tiF?YH}ehI-Hhc&;QyHZ#MAYCeZSsaJlmB$3Yku>u*@-i;#$762{%0-kgtNH zzkys1gRD6xJmKM6a`z*=KUtkG>8}TN<8(dqe|WonJdE$&zlCc>W>vUe8NYUi5Pt6a zo_c|{T)eAaI%YU4P+yzb-F5Gete*?iU-9xTea1g7^xw!njP&M3zJp!+ZILHu%ft8G z&;#^a$v19(>_l+k&`dOJh|=Q(8qN;Db5?}pi5x71N>uG%^}vi5&M}_ca%Iz_6<3pd z$PPe`JrJzc+I}GJCRx5P5-cI!Z=>S$$xdnkFq*HN(0pKEnylE<{gS+69z#&+{2EEhH<5v8a$t#>-Pj-QSI&bAhzf9f=YRGP<9K}~)(Gati(B^mQW z1#LS1k3?@ci}R$`ee)R80R(jAcW;|4wBQIP{PdY|8vB)6h0rXzty0q3NLl}4iZnx* z*lGg)l)UQAFI{eX#iO_mYiUUvy5as7^MvF~pQ{}2?%ENl1Q<8~}!&w4uKPHdu^Z2+7@+SrqZ-QcD zmb&W*mN8WZ5vC0vRI!e84V1ST$RSjtrD4pc`=rTJE9<$jYyknYs%2}``#5E#z8 z{3JgFRLh`8P#E8&AvpEoGg!u1G3PCF7(oEPngQ9ye91hnb4YP&VI*;KO#p1oE`IDF zqYhD8MhU0&FVJE{fK@aYqy+1Mq+igZLDRmUlh@}J^r7>Sj=L?|cCwp@DYjdPw!Sa! zZotB!XjB+dJaN3OhtVLuT7e%hvTzAj6_81~aguNH$>}>S>QzZKEe?uTldk-r4sGv; z;6y@@tAh)9cNeye6Tvoh+U;(NT7TmUUiVe3v6O@$Dwfz9V3cBA(olaKMtu-}UA;>! z-;@khfj|y28sp93*!g%jib;5UzoWU@#0L&bfeT7j-ZCuyiE)O_d8Wx`kip)rVxWX< z>fC}u)~In*bNQI|Xo&nokC5MT8E6vcXx^aG`CO64P09CfuSOurr3v@d`JOS>DI`5H zbT%pe4mBf0r;D`f9c3A7y~pQcy+f6VJ2GCilHqPUWXv7g_j}S}wIIm~o%+J@YwSGd zt*z?zfy%}b2)gs$nA!BmYRmV<_Zfa)*+%p3-J+dvC%bK4WoM4J+??U+l;&o^nb@1A z(P$MHA=Sv>Lg@(meNfM8<;vI(Qq4=&U%YFKZ&V-xkbql6zc>ZAeeg8mAbuG1l>n~; zFCx|hs{$fY`@x~B5s8y+!Q{I$4bp?ObOdy2c!7%?PM#Twi|4aV3!s-cbrfkd+H&*T z-Pz^nY8kl=R)3jKyvdi5T5_4c>%}XUWwS^k5t1+`ZsMN9ulqGVTD;Vk754c)(?V(! z@7G>OM;z_zARS?Ai4Ue$8#JqLYkyv9%|3A4s-bMZiRa5e#9d;yotCmk5iwcImDEF5 zRom1T1s4XZnyM2*%N=X<63emnBE9ZW5QEcy9P165%n8OxcS%@e@+g!Ygq>RHO6(7O z?OqYn%kY!g6BVhE0=r(bKs5FmBuo8q1XByr+`tdB$iY?`GP?<0Q=CDD7@+>l;vlNE zFIV|SYNp<3kngmWnA*AkTObuINW+E~DZ9MUZ6!oWxqjB@gDxs)ihjTHC)3~|(Qx#+ zmpWoTl0tLw{`va3`%R&OylaUXwkoa{Kt`|5dh-I@M&h@0p2w})x{iQB#cl07xzxPS zFM{HR`GSgjyUAF{88z8{?+*oSe{%{$MlKw1#gRx%N^Vn##Z$}uc^*8iqUy>se@fo4 zK@)x}`O*C@aC9I4vb7Q>)R?p&8LBE1ju1fnL?oJz?uj&B!b2C#i!#dAc!MD|Hop8puHpDx3fcNqIlOif9^k^52RgK4>3gOHy#EEduLpreVEevr)Kj7D(m9w&t)Hk z$1Ir)_w!dj_)()b1-luJ`4?;%IHvBHr7uGG<;|=8_)^f9e<=U#M77uVRi(V1y(ZPI zSG{DnPhgxa%~PalAIB(nm>hjL*wb`KrZ!tkaS1-xDZd~O)D>t3Y|wjV#0^On^e?Oq z9gG+o>Q4kN+)r1qr2;I!9%107i@DneoDRQ%s`gA|#6S z$y48$CihuvGFH+^Z^t=kV9bQ@C{09X&t~;NKX7Glb1Q|5cFUD@$e>=~D`^+8FIRMO z2nQ@(Zqig@b1C~t3p;eOOnnavb;DuV+M#!#p#AJ@q59b-cte>R=Uoq*nyKr?+Zc3MZ-)-E0i>ObMJl7z27=+kqpTiI|Oi7wj ztD6G+6<4aCN4eY)>S3c2-{8$7$$=p>(a&nWA4_5MpBHldvnjO!`)vZ z6F4o(4C`PD#x?`Go-g>Clg!;`2j-)%+6DEh4$N$o1+eOe*KM7vW1;{%3+Jhm=EJW<(1kN$YI4Y68GVgzG65cuyk{%w4$-okezXs0eK#SgfOHQ9n zp+e|%4#h~yMy%}2gAlhG6Pa{0W?&l7;|tnToYoNz*cXp&fTs6LiaTkEqcYgT3U0|(COCrOK{y+KR_ zt#%WPH!Dgb!F4SvK5CI6_@O+I%E2IJf12KYUs0y z#kVIg*(~U86O#JKglkjCJa@sT?<`8KnH8p0v(87UkSBuO-?%3e5TIcif%7xO3Xg`M z!4QB7xgQbA4O|JsKqCFhla|e)rTc~H^Hs)l-16q$3IizxC!Q|R|4}@d3}qg1qe)Ri zprE>T;8pq59%oI-K4w7lWx4=Qaiy9JyY8YG9}3n}7&_K%sCqg)pDU&>%y^?7N!*Iz z@(^(4OKg7qmXYFf_2sHpALAFyi|1?ARJQaC)*>labUAT{QR5iwA$L?m zv&E4gBM0>uXgOCFneDs+nygC1Ff(C2vP2xhm&o?Wz36%N(^NdCHW~PGcWT#%vFY2T z?A0GjNCVpXA3rQoqIn_KGcsSOjd}Y5o5XnfRs;@^{e_?qQC94}Yjk5iq{6R%)tGVa zRtKj$K_D4?Y;hng;*~?Pty{U9*o`%dx*BC$eIo^Jt%lNgrQY|qc-pc_*%aU3#`=p* z0$5Jd_RAA@Q|Cpgh&Xnyw=RkE?zMmbQV(hSAsy4je5huxZuUdRswBE_^hpZS?XEdA z_`#-yON%WM8K3G=Rl8phICwY9{o#oYA1zw3u9l_6oXMZ}MnCwS>T+rbPd5FN_8vm? z!43n)X0`f@{VacA%af_$HAr|c6iZ73j{QK%%83v*8}Z|~pho!!Rq{S6d?|`BQ7gYy zp3UPV0|*les{DlJ_fgW9cr>_YN4j&+LDtD|DSTMx57ACVLXrc_Yzi!qHD{R0O`Rkd z1Vd4h;64dg>66McN>_?JjAV1>81cSlV$ z=XMTJ&5w!1bro~tTNYJtgdzB3;h;IDtMJqO zDM7_K)t6?1qBK%AmeoHB=odcdA|^u;tR~|&j%?u9b6PG@S;}0fUc9vWqWR3Z`Gc)e z5TfI4NempmH-;^sb7l%I4xl(h*ZZ?+)f!0mY7I14!FoLHHZ9s=*?+Yu-`#qpJtp?1 z#?*IY5}B7N^Q6_PC@Ac6jXgFdMH?3!8<=gS=`AlPQc**VQg&BNps+j3M4(_`#JWT? z_>z@?&+Z{irb&GUHg|Pf%x|kfV`$jr3P8gnF75jEI)Xya)=WNAEr)a+rhdPG#v`FL z`{G8;vvdq>;3atqo|BzMq<-J7b|tsu3|IB^$im$^F4si_T~^v>&(UPRd4uqyWeg=|AZ(V`VEidS`HOlhaD40vDgv4od2R%hrK*r z<*~e)!ial8U(b*Is{YPc-DtRDHU zxwM4jd)kn_D4M-H5V_o=-0MK(o2H5{UMN@5;4(Qxb%zNh+vl7ZUZ!LaWah4}RyvD7 zP9pPYKlcKa*YB4U#NeU^9_p1b**4 zCuhc#x#Ds3=8Xc$udqC(1-!ls(%UTnkKZi39m@uW+FdF9VH>T@XwVS2l|b7e&%s$4G;f!uU%)yLa}S-~39@2> zOVmdY@HZ#d)rj%ep9;5=Zy|myS2yl^WpzUrdB?LUQiMA4N#Y+}Ew}u+ZDcth{0o_f z0ArrVbu=RT1`(%cwV;pNBsu*IKnvOpDZSe-ly`9v?i4?EAQe6Mk#UNzh z3!X)x+=`V08j}9H0$2K2Lw&Tjtk5H|Vf6A=aZ^a9rCMiZU_-R6K!Z^7ujGt|7!Zm_ z=_VQOY)rmWj6tyw7WPub(v2@fjr1d}w(7d$07s7Yeo8Rn%0E*!hp;}T|A!_7Rkd{& z<_UjQM?G9aTyNGgf5fP-74xEwvrQ4e`s+o|NGxi0*#-S-5pe9gW&7pyiH$82{j8+i z><)ceAI7xqVL{O2T*rVi@!IsMkG7OcG5!lf#rVroZGkI5*~l~NbvL!qMu!{8qkSH8 zJ$<^0tX-*<~t&q&M%^G64&NG z-M?jWdfsbDH?X;62d z@b4;d4Me?pzhb?62^SaDqiG1lW{HezMqX zS~=2)A)nx0)Ob>S+#=|?r%~p3@fv!;`c|mx>D?ZplC5yi^E}=oVrMN^E|s=PJ?kDJ zT-2O341aQ98)Z9Sz+1cS^@vaDsV8=8J^RZLDD?hT`&H(33M@VD6HQRGeo=Jr5f-T- zR1lDBN?y=sTZs*kZ_LSm#UY=cGLdKaDp4O66l?VeBdlB~X)oN(cDE?aL_Kun`qNZn zRSu#%rCA0P^NR9^#I9%`9QM(!q``(pAu)b?=y$^{aEw=YNu6LH)PM1y+SEAR&>6^J z@qk@bBna@pnIP!K429hL$5#0$BVlNr;ki%8ctX(HpQTf^lZmPdUo0}^UQS=3YlMT~ zmT$*XcWgjA?qQ{wk!%WE2Su7H@~P{Sbm|pZNoPfGUE1G$eQ@}4Wopm8T(W@io6T}V zTI10^Er(LTe4$OvN)l%}`-kg8?OWK&wJAbdd=&OhAYU zhmV$TX?Oz1T#9})K$T(V&SF;UIo?^e`)gd0M@hEfW$`Tn2;>^>^%5njc6QWseX@M| z_6h%*EBl0I#I^oFLfy_=;i*KH*Q!w`_a$z1f~Xi+v@Ebu7{ssa_G6@Rgn<>Js4p; z-J*p1s=x>l%1@2^J)Ih)!SP0`vm+7)^>yy8mtP4o-yWtgh7VK<+vHIXGX@oEzH$wq zo~ycX%Aw&S*pzQRlRB#IPeYVD=RsA^>=ZwPU=hd46-$Fm9eg8Kh3#zGSfV- zW)>35)eWLn3O_oG;16#!B4fj@xZXOe%Ir`J6wVmV*#NZW3Jqw2J51nHdLey^;iZ1} zIbdpk7s>JNqkAz*4xSb|P*ICfriWRiM!ng(y;Zatn}#FUR}>6cB>y`OWd5Yj6DTM+ z6pIM3foR@KUEa*mmh0XUB7qfLEe+lK9O;5O0RXMjxH-(>fyNU7w$*gGs^38*E-Lj4 z28p`Et*HcBdn%@Gs+(G8qJ&RNHSzX1%`#T6_Qak1M{QNktDMD=hixE=p$Y3?dV(v0 zV5NOE$?s8Xlwc1i`57#GkG{>J->j8 zQh@mjE(KVmU9Zn2yNcsi?iz;Dg@E`TVY>TW|Hmy9ji)~la{1g*E{Z}>&o~uq3&ar; z=N|3_33St49-LmHhC-v5TZDf2AJtnOxLemMuvKfYnkX;@w9d;<3KT5GUpM4N!bR!tQ~f-h7HlBJ;ga-Je@qUjbujQ z4TqR%bZ7m)F%78rR=Qntv+Y%okQBxyp)~g?R8)Bt6YwUXPalg_2WU!YJJ8f<%DZLM z#sX|-^Jy!^4KRK-2+TV&naf@WRVP{Q^%XM~=%XTc&ZLeB|5gHOBIK$_4ofSmgbG;O z<)m-GC{Q#_IzgZlt&Ynk2nNTEv_jA?s=~GA_oa{_hkc z5I9$GClFhDouHr^6W`(0_82xc?IA2u(R?l9u>g6ag7G%X-#wxXrYGqiQ{-vPlWA2e zudPQUj26U#2HS{HIhSk9)sfZ)>1Cmel@-$h_u~-G3)^(PuMTTddecC!Gb;>QiL-7z zM;CGB=6ift3LL-%63VE48H`!YI-@$?G~X<+R>rKss-9^qIXP=%9s9InZAgiHQmG1_ zv=sYH$txUh>>pDIL=pD$V;hrpR~vZey&hcPwu6~d{sB~RfDybKQ7UHPiK;LON zWRqMucD-?V66(0_UfIMQCRA;L##|OPEfG{jLrG40$!M9WC5f(|M;xno-16sW$lmB8 z-JlLGT=|b{Bal3v3v@B5cx7#l$pUy3dQ`h_J={I?ohFbEqqRKDPC3F*D+nk;bX)Ua z?ly}O&?@~}A`iaMkc!(eJR2B8z0Rg#R*%^FM0a(zEtrz}sxs}a@AjXumyb_NlY&xQ zeh-Z!wI={tpD08{^{;973RGxMp@0Olz;T-2&z`2N;dL_8Ij8S9dX|Gj>^{pOYH?w) z7^GefcNb6FNVZ+?J}u{O1{z&P!@voSe*QOb^|f=w>)rWZ4rN>p(sk$j7jVi8vuY1t zHr0dhLLrKM&|{zVd$TVfA|z2Iua@=qPEWcMI;X<312se6s`Y&yUk&v zwONgkRcgyj-HN(+)T}AvdhJuy(J3^nKz({|feEL0%~Pk0?*UMg9$=3T@C~^}IZl~= zgw?Fm<1bM>-?vOeleKAaCq@Qrj#Czbn?@@sqD%DJBjVKL*cL(h-i+%h%mSz>Bn{(Q zNVoL1&gN^WeHT7DwHq@G| zRDPsuLq;ebYUf|8#030oZ;?8cCm{KIj?b?SuFyX+To``}v{9aoLa{5Z3! z$J-LIqTZf8AuQ@l2+6B_()ET*7hr!i@Ol2+ZFM@nG}X^!u3_7DKmo5s(|NBtBr2Sr zUJDb#$pS5QEoKQpcq*D*>f2_tLQBslYkOtih!@TKVUI~dl4VtXZ*7q;FgQXPY3wS~ zCXrgO!u60xd`S$4fScO#IiRTDXVI{Hk1EfZeKrVJ+i{MKAUXWYVdhoG)j;Wc!;tDr zg|yQNWET6Rvt`EhQ6g!;HPlC70yIza^cZY+9K+(4Ti%)1N(&?9Mw2Dp29ZvCq<`Bk=#>xQ+G})@3_6pC z4f55G1s;f!v!%B|d8qjb`J&e6Z!z{RDl$A4<`$NM8D7yF3Sce`<@OLCvY}rLpEce4%$03nU3rncu3^2g@iVJSG&Q%-_(G#jjv*>zzp?Njh+NszV4i%c< z5oIuRl%~rxRzSFn6Uo}7lhAV)W4l%q_i^j~^E03bK2GvHm|YBCe~i)M=!O_oucMfv z8o{3KHbGu@^LFbf69I*ndWYW6_JQw#X>j`G8w>YjWCObCYv2yi3_8oQ}eK%p5b|V;ykg- zn&lXx^KeZp)RyJGmHp+CSA8P|F8_>@D)K{zf-%Imy~=7jP!F^{RUdfx@6z$tXRGA< zevUMuyS(J&UJkJQ@J0H1iZuIM@>4hB`Ox8y+8aIp&}>z*^dRXam5HEodmQ=|gj(oY zYz11XU*|o!8#*Q8*wk#ifc%YgT%x3eT%5)#U1t{8GJ7-HoFl$Hf9SU3nD#c`!<@pQ zHhKQ%mP0o#h*PhLIOQS0Kg zX1WpV3iFS!K+iKX2!iQORo;`42j6dlJVJMFlk72#7J&ytjIl)v04k5|AvJnkL_HqQ z*265^!_}2+wnzMkj(A)9CE?>hf}eXNLGNSZFLg=9KP5gfPtp;x2 zo8tE$X%wQ&{OzPnFQxyaiRA00~;(aqbR3|=!OdbL+ic-KDQGSpeV# zg!(QW6R=2XSJ8lb$RXk6i0dZ@KM}i7k@32|1i;XwKF}JPbd2yq8x<8&^O=(`q;it! zn+flzo?H|EX)0V9AUZ&$@d&FVj0D0s)md%~N^4w4l?^ft=?II+nptfg7v*{7g+5tj z-hLoN>!}J}-)=r>ia>3SN|V7|T_qXDRVNPt+V^G8AL!GAymP7Nk&ZEb$qz!D|M&`a zOf*8f)FmE$$`!3a>riq)TH84ve5rUeR`EFppdMKQP-~_g0!zJa(y%{O+6ly3XP*m6 zEAUdhy+uUcym*Zr_+VUtV#gi%5=T}#v4+90vf-DHgU@wKh;IY7Hw~)uQMD#@6}?n5 z@S~7dF;y$eE12IV!&(1O8OGa&WGa`{(5}D-#Jpa!{9*OGoZ4*=%>azSX=Q>qk2X|t z%q0Kxi8Kb$5dY~PBxMy0w!@$?>9b{KJd(CpH zR%49MKvq1$W1r_&wb!@(qhR&*0f()(bEJCf%H6ZYNo{q$_}V6h>eB&%)(DIC%*#qk zl3r5`pI5b}!dy+{N&t!8o&Pjw&GE_3R?BK7kcX4M!T**8mi{E$8~SNarbOj2rZ{9h6k6SzHlB&Iw3TdjGnjJM(u{E!uk zxN@@eA;4~s320yKbB=;tQRC#t`(0bs38GEuX`7JY+?~Kx=uSLbKrb~+cc%PRvRl3c z1Y&K6H^>I#YG-E_i7sj_P4$*a)Q5J6R3JK1+-b<|j__eB@F&L-7bWr~vMQHZfJ+t3 zMAnuqnAcgMRCTWWo_};o_Lf&m!&P%7C9!?3o_m&jDk<7bmG2rAEk1Q6bP%(0LueCq z*{<FTwiR;J%smhHV;~3Bo z{X+T)V|agBloAJKHA^UupFKQZ4SBfZ5EZC?dxW6XbQDO939m_zmsAXsck`7MRGc~FUdKFGFx)Q)&$m26 zKv^C2ZW-M^zHIx>v0$Ff%yW7p9qUz7cPx8EyML!iV-_R3$3sS?_>M9P(TKds`Z(vc z!y$Il1MO#D5rbY_OXNAw=no|}(sEY+xqIN{T>&fwz2t%B$h-0#`u6oK`g5T|ffQWJ zKB!qKU+bxhwC&)viC24rdCawuuwUn}j-uA2bhIku9bT?e4J-yQcz%u&)3*1f0>U$4=DV}`?(O+WJM?0!75Dz@stvN44m zi2pe+9O7$bn)qJO<@Uqc*em`-VZ=g}UR$*Vsg$+{(LnH7MVp#88x83dLTcC>?B6j& z$OVzPv+3Ns1J7=*H_Y9uW}IPl!&9?i?2y?m8e9+O53U_5 zi1f5sx%uZfVFMx@A-QsECkr>Juzv4dE=+G1yX4de2sX3wmFlWgMS7MIecX*(a{briyV*P z-hl+vWB4LzcA{iLP-11nGh^thR5B^URySdF8eXEC{<7Y?=J)`a6SKeK?ay0TJGVa3 ztZ=#CeW@6HBZL||) z8I@2=FJeAb3|9h$*1t2f!%%GLf~jFG1;Y>%9hKpAhZZH$_Lt9@WEz6qjkO6& zcX)mC60hIqTUU*i{RhaJK>@I3av`~bsr2Bi$$7hr>>WfaioCY& z!Xi|^boPHrJB0jhpl>HND|FK7-_UZyl#m+n&Rh#Eal~+OnW&{rLm2ZJo?~8aRI+8XdhAx~JdmELU@XXP z-RS5ltQd|?7d;OXT35&A^!t6CiDE_7JYe{;`){DpCkUk!EwgUweLGj>a7c*WXI*=< zR0>LO>sN!@+9Vw;92Ig0g>6>ey|dHz>=;Dt0YqMK*V}toiUi-~SIpOI>rC zOYBz9)5~C#vbNn)^syHT3s3*Lt&?MUa9I{srODD!yntAj1r7DoVy3H%_X2m4N&F}o zu^aaLHkJB@KPIm`gc)2_#K^Q*TM^zh1cbls0L0J5OtYCN&@5F7!0udPw9)83vC)kh zZ_K4|#LiQJ*y4^Bw>%x7SIZ2RVt=HWs0T|r%i7wqc0}l~tyiJnwv>C8pcPY$?#&8u=v!Dqj+lIg6S`aq@0#xV574W_;L3p^gJNhX0_`Sl6QJVTUs% zdOT^8^^PH^qUe|n3VCao6-9odhx^!*i%2^Vcc@AKmQ@QY__4#yRc}ecNcD>i(1(|7 z;hI2R{acSnjPJhucUTM4fpfn&uS-6o%k!fT=x};_ti%9xyZvzhEX@R=BpWpkYrvee zbne|XNtR|yCZ@Uz4|*S1g4M=T4Pl9&L_j$nb6#c!s?AcO@>F0pTgFF zgdM|c_wBsPeQxP?+81t8TD%66v4tc*h%Fa4PF+;^9P4Atx=yO#evWK~;u;jeafU(% zmGz9&+i-ITQG<%>vPQ-HjoX(sTgv*fAtmko2@M=4HA*578~@#wgvRu^b%`i}!JVa~6RxVoUvT{CVxg4Es1y7LnD z{&SO^PCyZS8+X8AyP|#6(N$uJzixt%K_%lgmEi1I)m6-8nnuE9!P+Q9D-O)i#ZijD zT@u2G_+s1(qn;#>$x1l?n5ipDQO!!*xgRizo{6`b#^`2w7oGr)+hL@q#w*N5AQimM z?-0x~?EEdh?D4NN96rMDK4FIlbK*|!9`0zKLN1i~9oy3@ADACQ4tJV_Uf#UY4;9pA zv!&(mGck*s7u~l?<@=T67s#z_-^W(M@oOvr)oEUoo!V%2hd33CevI8gmqJod^3r!$ zuWKkwp}Q)(6GvH=D47J`GnG)rXU)7sf1#SySDAaa4gYZ2hT%z}k_SaeeUcQ9sKl)% zLja+~Y)~e~kCN%^p#n-TjNp{eCNYJ*!#rscjmBQd-&(w=JFY%%jb9}YW&AMaHEUJRvLOk&wCL~cnYa%a!c zx~qbl%d0mNDsU<|MERfRoU>XrLcxrCVX~p>->0b`!*L%3>fcjrz(a!>7t-BL2n!n8 zV@C3<72_INsDx9gTABeQ^gj=4D5hsFjKK@-Onvpm);4<3T{3k_a*_O zAmo=?A;PlXJdEVyat8IAfZ{diAqgZ?0iDqX<2~9RlU#aRo1f=%G}48BVixM1y^*34 zD;wMIR6`L?-XM+G(zrs-dWh99OEARS0F`Pfd%jWIhfdxhDr*8;uyW>wuyzT8&|b(@ zEf?7P9So8ObgAoJhF+&+(n2~kwp$m&Sn$nMZ>VNQn*!^Zcky^r?iCo9lu9=loVKc4r}<{|YKoZWLAW$3-KLLkI$nEtc7AmoME{FF<)%XV38{nTo@ zv9;gSur!SY({;(Ou>aXg5hwl{4p;9RNh;kd?vXi58hG>S>FRjxW;S$CjUG>q_3 zzNoY|mrx-O63lHt%Nov$%EuIiWwE9?AB4{DxlA=W)h~mO%|)|aA+){TSLx z%+6e|bd1&B-?_>wlU=rxGVFBi{Zd>*T_5jXM4$}@65!1DE8ru5>@T3I+~Qb@9WI?X z5JgMQ??N&Dg>OxVsLkTtT%9l)Pi`TWeqVFjqbJ(>x`gJJ49_=9Cr0kauR2y!2Hls=r zxXHN8+Dbh?=miYJ$3@Xk*r`$+|Orhk_VVVdzqYfLwRWf3+`S~dq^{Ks8K38Wur8r63PvVm#H00A$~W97<>OnUR)lS^_$-Ki zd<|UmlajoRV*Q~I*uOuwpXNEAi(XP8sHw!Xw0Zig%gnl~9;5!!c6qH~Mw({Z-YkaY zCj@vbUmj$pOCqZ3pvuUWaKK41n6hC5Irle##9x6cum6R5tnC?e`xEhmFmS#lmE&NOGDrBiO_bj+>u;hpcS} z-gbh(KV(1(On*c=a@@7}`VO;z&Z5!>lHp(V_XWSROf;1x`&_7GmFh^whVjOM` zvHI_}y$cTCo>kDDBqIio;KDRlJ+wu|o^yLQu@7$-Tdbq?9v^mP8L`8ppHva1FmKqfG?5gXF#R#skgTyUXsIZj-Nz6r9v&PtyPp?y{<9)X_j)9P z&C=M4rnQ%vpxaU?hrW)C%uF!Pv#3wF{RHKg81*efkqr$(3uM3h%%}wx1w55p z-p#Hfgv-f;#g`#G&lrAGt7T1*-A+40#L$0J8rb&G+syis?-y`JM3y@w@P{U*F|P$ae!0jt)@U=EL9e{f$o!jaG{ zB_HD!K8-+u>uSf#cr&q5Y$;{y?%z0%n;PK9yI$qzpIeKT*^~rv$(Q-XVM>)BZg`UI zK*tvI(Ec+gKKuN}sv&O@eWVf{`T7v6rct71Ys_ndF;=+Eq}0Z(9=^At1L6*{`4qUz z#{)Tpl=j&T3nv41^>xY1@VS(IWSJuTmZS8!2+Jw(<(3C!($bWJ5mvzrH&}iLo4_$a zTklTVbrw9cf5Z4@pxJgPYx;-r~d)Aw7F01V68ALlp?a2^^zZ0~aiktNgv^FjuAd_G7B zt(@!WRuIQ*XPcJN$TfKQOU%T_GRq08KB@)SzC9Tbv zil(JsXs9tqp`h8)T22YO8IEL3NI7-_v%BPtD{vL^HtA;!43IG_!!eMUW~86MWy+6) znJq3x>>O{wvKmtKTpXE%Awq+Ol&aQR^an%uiX{31Va}6PtvcRsLU*fFt*?WVaLN2G zHOlPqWAoy6{HD=lT+w};+wlgtH-Iv~nc?%(a)kzo(k7H32DeY6!&Qxg4G27Vm!iL* zJ?=|k18na~U1j<1U;5bX=Q3L_bgMNf-3@066o~P~6orgvRP7G-08UiB_R~i`TJ1O9 zEXvtalZsd7hiY!Rjk4v8kUwTjVL5M9PxT1$Za8!8{jMIjW7mUhiaPSQ@tJqA)vOi? zVmDZB*cpAA2r`K+!9b5XgCyb!Sk-r^toru4AO$Ftpm~M}U zx*}_~#Ls#kpvkjqd4{!t0c+i0_Ow)KqrWMT6X&{;ac#q}Jky@)I-8LXwTC10W%PN; zldnVwZsTuX7s;^nQt!N$X(!3Y}fbA8PWQ7)VFL}8f6Bzeq9>2@ zlD9;e5=|m4tko!a4=hC+V5^_6`x03(U*@@Pw;sV;(gaP+#HI99F6^JkvK}#%t_F=L za9q|SvMb+gM;F$e=r_7|10}3FV4#JFx)H2qo@q5A_7hau!}CsjQ1ge-*g0ovyHT~L zyK_N}0C@kPN;0Weap6jtCTlnk!wAY2tBh@)%xcMX_}=XCbcNj|cN?yWe|b z^RFY>FKz*pKIp;WMQfv^iR#qkbmQ#pKb5b6j}~!Wz4&fa`|`pmqF5%1Co35(*kjc{ zbKq(WntoCI02nY$2(bIVO@q7d=Q(S|)+4|l=NQ(_oqiR?GO{SbiagB)St@4HaJ+&1 z9C6DAy0pUGAl0n@x=E)tQ^oh4#79+e^hOl*EzBi;?Slim@^k5*7wn#Ye150k5=t^u zPLTW2G7gIvS+U1QH#{aBJOQHf>+v)|dnR8yx$d*vEXeYBO(kSr+?7nA{4NbS z#)}E#{TrI(iHBcGk2Tp~BG1I?P7}e_>7v(00Q+sqGcq(_X`D)Hj?`DGNA@r#GV#ycf%-hTh1zeeG9I-ya? zjY?|B0d<-OmYkt%n?VjS1nGg;i$h)sW#6I`>?;`o+R=8TsN*|-{32~S34`KG9Q49r zMzByJhk0)xvSMDdSgy#>i`~tzo{$3{wBiV@Wy@6h$W7NFlLVnNbSU`QucSa=1TU!B zO}Zhvu!7 zsu{&0_TWiA;ryp+O~CQJ^tjA=!O&5KoO#P9<5M=r4v7S>D$>P-RS!Og8S|3iJsf;h zh4YuMCEfCgXLQz)X1VPE-DZ5%`!PM^gC)-c` z(YBSHJydtqQL1z=5=XWM?ZY?YDHGN)sWh#N0OK_OmYgpuRj%s@>B1LZm4&aeq963C z6*bFF7|_+k^%BJvW3zG2lr2|Fc1ho|#1gi&^tAGWGPIj3aIr$o!fJBAGMwmIUhzE%gwnc^@?L3Zp;{TSN0v*HS?0$^J&iGDn z^bV-scZ!-J?#)v!JFvQSKW%XJeKbmqQ28=q#{c8-T6jp>3Ll(_Oo!s$TRc8teCKa| zyB&}|rdUg`10zCm|IR!t;%87eD1kXf>kdQVYiNwnA=_4w4 zeo3Jx7JNa3Mrq^mjp6&1pZCAlIu7y&$TMXP4!CeGCLGkl5pZxsqN02=-3-YG0%GC} zW-auORvMpu6Q|3OUQl%$<%)^k`T20BucR;mWFr>W$|jNA;C&lR0R1Htz3?zrZ78v! ziDB)Y$Z@6hj&tQDaCwgA+;F6^sG=-D`DPARIt=Y_TU5|V<_5>V;PVu&=`5pQNiv8B zn*9=hNcz}BP)jA$1s$7aC@u?%2YlfW%*{B_V3Xjzo~s&eRH}~Kendq?e}UvUJoi}1 zuc(wxqR}#&Zg<+UUaQ7H*qM=nkS<8p2?&ofdlar&7G?7`P!A(MN0b=wn=6Gfw-RAv zqJzh6f?W@I#8{w84Ep$3P=uuonzksi5~$I$RF`PG?B>Xh$DeGm!r@NuJF#8btX*N( z3W!${Q#6$nAhx_y=0xNDRF9~v^oR9rA7r31+v+L`|1 z2LdX=h}4b&xi?LR<`ol;kF0C;iQ|RBEs!4WSe6B}S z{*84W*u3{6D)5rD3_B9jU-)At!I^}Vqjmj?#)Os$GpMD_jT~FrSQb!?)&slvpl{t; zi`KaoNsGvJ*&Z|TaZ>5ICa|sOK-Uua8VSW@bX8{M5^V`L6U*wlt;RPeZ3sor){U5^ zW?x``pXVA!58^Be`qURjb8Qa%i4PE zLwI|+5iVcK`bha0XsRu0WoffsI7Z3(pG^O|snZ(10m0iK+mNNGmurx<-y4H1?si4wT_CJTHqn3i&?aK z$%U|w#Tu15T2KdisTC!iou_Ja#mX-$Jm#0-Q+^qa{1Lxo^Yw`2t(KxNyA1GOUTeWYd1K3Co2lbcJj>1V39b8jW2&Dxl;A=6wlJ=8jbx&RFUZ z6-I?8xO#~vui)_H*A}X+cp_zXC_v1#tur5BF&4*&t+o}#dMApJ4SVJli2qs|U=d=^ zV>M22a6!`}D&FAJ%W+mDbL^`S)ltNw^DzX1t-)IWi%NwzN)VLV>F0WmO_>1s5J?U{ zZYBGS9!=3+A)px~{b z_|Uhl`d;LIW4uH=JaR&h$CYgB`oy2!m@!Fo#CtS!T~Yjb1jYRzU(fXwmE$wcSA8e@ z>z7`sajDfCTp+LB^O%tI@M2rN2=<6`SL5w&rP43ERsY~S*CU$g3!X~jzyq`jh{@JB zb?ns5{2`3gjAsEgF;tbA*qWZ=+gPqC*vM%@vE$#qJ~oXKMkWqB6_~ZjJ`7lw-QsM| zO#4(fs@mF2uOS+x2YPjdU&0k_|FYAvcFpK${a6CMpWFMpao{{e;1GddEdu?SJR0e- zd6CLu500s zu^?+y32mZJ@Dt6AvW#tH0qb{|`vmA>kS zVpvx`!}N?jJY#Fs;PLHuv)hQ-LyCdfMt$>^&J!D{HRa*XKkj4mUN&aE;62k(Nvsnt zo-9q2e{i==pzrA$BJPc;#+`FC$XMTN3*!D+C$H3+C-_35bbCGsL+Y^>+cs+Bq4P{U z)U+EJu)z(|;VJFWu4mfu;Z_qo_8ZOhu8vp;aXftY`upMa`?vX6N>kRUPRFw_M`VY@ z(`W=BWn31mLO?zI&lUu~CIOzzYt;gt^2;g9+WlCECU|@*mWhUD8A@y@E29Zj*g`axjYO{ZsJ7$OWJ4Fw+Ej?8a;;HIOo`h^;Tq2l1I15U4DaB#D;-YtC+{6Kp+>df|ch*?RdU*`iNqlgiWzq#$o$nu`1iHLgbC$ascq@+UYR z9JpV>>x0Te1P&3{5&^D_6Ed}apAds)0pC^lV#)u>Z4>M|WkYTNU|3)(RF&{Fc&-Y< z5^)JPm)&?F-lTM{U{*(;M0;t&9BM1LY|rskUnuFR5{GQvSs+t>!?-Ug}85zs2ppe@kF1AMamODMEHtIy--qhejxR)sP;!S z*{Ol$Nv-I3;w|+@g=69iF{=i>wRo(ifnd*ywNC9jC^f>2gyP*Rt8 zl)$Nx0+^9P1KU=kEApHx!%RpGKTG0Y44cUk+nu@9;=g{a7D|2K- zeaxd%zQ~Yt(brUPj$|FdI`(@vhT-WKpL^9(J)(kgF1kNy|De?#v{{R{?7!AMgo~2* zc~K6_mS4K66F?qh8zp4+lu2I4u;eHq`zlLo_D3(iG`0G~L1Rp_DPz}5d=*=5 zqD4>(F_w7BsZO~rXykZ+sfA!t8F?nqK0FhF^-9?b2iNIaF&pe#mkx^iwr$Pi8e+3_ z&stV`T$2P@42fLiYXT1edQA(4cBq=Um1AUCXcuqq$-0K^)OG#?<`98H1b(v!tevib zsB6F)*^WZ(C>_|hV*|Qp*$VgaJvdpAbe`y)pU6_5ej6BGXk|Q*7Oc*BUX$9HxCMX-U zbz;Hc<5V)=D%#8p*dC58a`$vrcVbKp#qy3vV zWY{>4oDmNbd_cdbVa7I?ht>;4`2+wj6eTwB@ucteJwiO9@>X9)(S+X}BbhTjqQc`G{Q5h7Mdfbzpm$Yhil;1x z9#N59n&3$R6GZTA_`+m6;qW}8fdMxt>%q0<2w(U*VQT20wd$PPwn|dG3_!Jy0X*uB zsDUXxF_DH}7?Acs04S=UGlsy{U_!FO!zP|WM{rd~hkuPvU$jyqR&pqsPIRb5E_aO4 zm!9NGrAsECBr^O?hL|XNa_Y{&x#XAsd|@JVSS?;zU&O$y#@SKH;cp%7tObIl9V@Oh z26IgMHTlPUG%X%c@k$6K3&1g6;t>xem#j#~l&9K2qwylQaLAPri-*!TBBTJ8@H{K3 zr*Igwj&?{pZVKa9Ul!4;B8K6N*psDUoTxASh)NpLBPwpITL5$EPr8-kS(36osj5<5 zPViR8q4ofM9;*-eS_oqgvT~vDm#}SN1osI^g&UFBvwfhkz z+M&`uU=mw}ufqeZ2BN?WT%V-r28jQ&l8W;_$xj!L-{4$ky{gkA?lwiyd` zKJRd)k<)Cs)bJg!jn=V^;2t_h<}FZfxMDY9PeT*jZ&9i7_*>XM*gQnw5P^@0K>jbE zD&nH!%MI63uBnULh##Rv#t}KcOw8*e=f^=f#E&x>wMXq{htVnUMutex^8^O|2 zP#GQfvLL6JRiZwA0`UYeSWJI9i09o$6kqz=_-tFg$5j+FT@XR zVJ_^)trf`(?vA<32G%_;Ndf(NZYSLM7kA=S=iaOy@y6?d@eiKrK$1IAhl@=$$1DPe zH*L;p`glMWTf`yo#^`{w=_m^}m*ClTE3e;O^~2*BT+zkEkcJv7UV|#*T?=U+a@RIjHyuz=VJIHsbg(ah z9NB=veZ=^cn|Aq^;vnfqdA-fmkKP>l$6f0|SM4o&sR)JXgA3_#gc0$`0SKENYslEK zpzA1{Vv%YSS*oAAhu|v@_v#!b+uwJd;YF zI4V%%ju9nPgkJkE5dVcY5=YTZ)kj8D1A)dZkIdR8X4~qFQcpB;md!Hs1OqLz*LlGL zwk?VKVyIZhDkSMvf`b$9=7T=o{Npb=FZF2IAHEsRwUWb+XlcCj$yGMA^9~D=2z;WI z7kpYVM8RBuDdVN0;$|h59J75(!>A2D3LmqXg5#VBJT&YTFUg|N?=+XtN?|Zg?@kPJ zP5I?N|I3e|kWjjOcd1on@6;#y94`M6Fy0yGTFOCL##v#)Vh$}=v8|Qpd`4Fh!Fo7R zjs|A&h_B;_YTg^$1n8t}d&3zB={Y#%`N-PFHpn*SD+WrX0Nu>nKG% z;Q${ZaEQR)MFjZ&i_1uV7W*vs|CdWH_MA)fb(}8&GaJny#3hxR@ayW=F}=PX?m6fB z=k=bq;3Zu%xh0kocR6R6$xnD6Rhb)jl#;)xP1$)Cl+FkZ-K*Kz z2a8fO&Aq}N4q3MiI~%sRPv#g==%hj-CIi0LhGR-E8DV`t6C9P)VD8HdpjK*VM(jW1 zMLX6c_*g%LlG5_Ax}T`6_vZ;Qp?t@PYoep z=bwG%cX045|LVE4$`+GuOkTf#uY*v5R`-M+<}2O^B0gxhKkjfSD53Ddtm9#Z!w{%$ zL#0tZ1cPw`V|c`oNzv`)r4t-{gO{&I@T>Oe#l>*?^r{RbaIK4RcyApUT zh+o>-NF4CagzFEN{)PKS?ebS%(5L<0dNuLG_tpUtWI{;KKzIKOCx3i`pf|#27vXfD z;I9cPMTyhLpzz?jt@zxx(x5mqr-Tj9S3Lhbe7u=91&|p(Eb<$ zHnvu6fOW*d0@h>Z>=#e6?GwJxUr=>Oe^V;-Q_<%3=T~5ikgxG#QTk3RCFK8@z*B-l zCoZvRF~g4aaOivm>nexXi7J0p7Z9*eKCK}wZT4TDs*Ih?FTN|Bn$TY>!EyaQzVKoT zG+aCH<5=l&6}jv>HZJuzi&s!&Ol(fPw95U^*+&ivc06|Q6CbD-U#}-=b`HAq@<`lD zSzUzOKkEN`)g4ET8`5$lUEaTbr*We5Q;C(E%NQsA!irYb-020d_t&hRaYLj?`gj(K zjsIAb_#-jmfij!edtH-&ZxVP2K9v&#Z;p`r z=pkCR^M=l{(0J3-IDi_Z0~li=xhCInJq#>ORU+N4a#qz-9;Q2n*Rl*+tI>0g!l&h` z&e;z2Lj(>H`1^>!;zc6c-$)#xqAithjdtPi*|d&s!q)41mpJdJubqKH z!ZP9$VGP))2c)BzD|z8H8zeZigFDtdfMf=VjFU`koM>gb6C7DLZ|Zb&bZ=Sd3+ADB z`Z4;J`(3RFxSQ<3$Es!SdgAVyBlWoVE*t@f`|rf~rd+UZ+=-#uz&&`~tRo;TxD`|* z0%tHAekC&vfvV%$LohC+VlLv&9dh$gp_^Dq1(v#*PHXQ4400}pTYIzTx z?(hLv2?jrYLP=sns{u%E5LYIj_{bL{SS52Nm`4g&;lai))lBqAC|$mO?N9k$=}Qtf zN_OzFSANYVfuZLO%!|LCr0U4T)B{j6bAADLV`!F_ArLDuQ^yWO&#tLJV1tsIJFzNR z79Wj8+zAe8a{HNlstv5*-~z$B6TIQ)nBcHrZ$~_x)s*Rr@XZsSNnaq?NdlUcv5b6- zqu{9afzJ(H^55^*5R;Qwnmpf^|C%^+p=V?t$^V#SqX$nytL-J|RuBi&r(cZ(j!k&C z12A{cwS{boTSttmP*<=n!x~R#N{sO$U6Oc=H#B-IicsHO+MVhHEB;^f6fi9 zSya#?M|gy0y4DDVn09JmXVSB3aaGw3lf@wrXJ3q^kl~oviEeJ~Mm&|^4-2BW4u|fc zoy4GYT0^u*Y=HNRLv>Gj!b%P8r%Gy^9O=<-O5J}_uf&JAubLs;o}deXx@qBr?W7PLlP62e_aZ;$B;Gv%m_UFfEM)M;F9uen!c? zmG?$1PUhjzGe@0yfNzzNPdm(qxL+d^;QM-&ByZSsC+>1~0NFXET44%X6PGc8Ac{H$ z<5sCdO!$osoM@gLgl)~=#l)|iqN+ zBiFZYG`WaLIE`ehIWP$W`(vyP2R%HA`%R&nAyez%7Pw*-L4lzbTkDL{fdi~iP|31= z5(1aMydIuBeWnD$xrI*7&xfa9ed#YW+>jX1&M&@Ba42w&Y4sj@{lXH44Z(DC(X zdA*z9!!NIfH{aTrkjEwAGL}>!SjJ#L#sFp;tiy8h8{K}) zN=XUf_&XvA$9Pv<8S|0Y(0ezuYJ<$T2pTKWYM<2!((R;%6M@7AUD;RIv|SZ;r~FIg z=q(=azeUq#{G}80BEyV-sfvybvRg=@X(z*y!3Ep{^=xH_$hOd?t#ZgP8rT+OjsLWV za;H3=YoFtLujaJ5Z1Ha9n^KB@uIErcMBosCzmEvi|9d`j^?%o<&F8WXTUa)UuHmu5 z|DVko%3CGb=k+x$iCB{DdF{9mP2U<~yfinSUcmx9!#@{L z+29CxTB;_-T5%Dx;7CqHQX^Ju9A#3&$qXemPPLyM=|^e(xOS$!t>DnTCpxgl-KO8Q zq5DDZaIjjL*&E!*wm;vzs-}22Ss}V^fAij3yrQ@J z^^!scyTp)YJ0=max^@)_dPPQ@-l7he5XQ4Q(xleNun*YgmEtGQu<%$)iC1YqLyb_) zDPzzm7hp~M(U`12z)YewIKK_DtE&bUty43=)D`B_z@L#q90LtZhcIu z-%8=NKG%4Er4CCMh=c8fumm|+XGc280~K9#9y-(ze<8-J z9WICWdI>74Pk0>VSgRq9Suvs28_&LZY29xmbFEbnOr$i7ZM1o77wIJF2AK_Pu6Y1a z)0PFSa%Qrp0b zH;ab0O7J4k4~joy3JZkEp~XK|LFjQ6Pv+sRM^w<4o4s&dhxjK-d1{pX1%oa6-mMUb zShNUbs)-)@;NxE^G#;MVkYo3T|9(V8{6f<0^$rdaQvM3enJyd%;6MCmmVPUllwyL< zIN;io7n*(Q@d;Pbs>TrGi5Ji^mb5dz{8gjRo)4e@;djFejcKpk&={vbNnTuC>I+A* z=@$e`_Eh8UsoufCc~TpXqxoFI7j}^g62FXiySW-g-?tkxM`)15`5bPw<@su z%GVxi6sr<%#8f`|KLx#nNY9n1N+0MV&SNNg&xuzjaUGFveDdydT}d=$&Nt0v2sd^e8KaXv+##^1`gzMSvppO& z3d26^kJ);=rK50FM{mF!i@I(NN-wj{5t!1j5AT|e?Jlgb9}tHK93t>Hj=-ZTG5z-?$=5 zg|3)U7Tj3ZfZ*_b_!?~=#FB-?EjG({@~JrB@?f+k4F7yTSy*_BGhiRh3gkO{m&{jK zdEwO;9Cbe#kE8G?%Bd3?XUCd@X@7F8R~$IeK|K;4=cgCLnSRhc@jKu&H@>|dK4``9 z<&D1ncl*IBHcsxe%0us(yIJWgjecrCAL!5{+P7NK;rDRpo;cDQ9ND-Js(Yb;_}P0* zYKV{fW#6-r(0~UVa~GA=xo1_|phP6BI1hcWGg3Y|drn|vqqot6K67AKfVE6iX~noA zxIRWhHjJl$eMhA=28pB1(Gu{DX{6K2a0VR8=t5zbN0>qYgiggw^7%deN}q}4-4)){ z$xO!U5fvWo2y^$t`y;I&(Ca$x{1ug$3?h?mTii(pW6G154B9N3dfzRf1GwBeFfbu# z?{PkU&7~dMnS`n2Jr#T^^W9HB=`oKd!@1r&uu{p&2!6q~Nln7vDgF!q{I5+K1mde= z#iYnDTnH+>o}3qR=Uh$NK{>ghiJK>)Ua`TFd7UKqq6z79t=4#VNVfZk-c6+N2agCJ*Jf7muW(x8~eAKke zYdOXRV;gxl2Urlt){{{&K4$VJjtA3qU>r%FM^x@KG37*bTzJ&Lzp(3Kq7@LZ`JfAc z6JTW@f}Nl!xK67u=M^@1haKcHR`CKJ@!*R@Dcs=F121rHBb10DqgVc~m{^PCPsQRt zlV>L5%G$=^e5D(-)W%6g{`=KK?r>B4038~?%49gU2!9^5sx$JlEE0z zds!|tj$UXSefE33M3e-i#uxFp(OACJs-Cw${o-+R@%fA42?;*QF>aoH^@WUT%xnC= z^7!X`!6(KbA#>z>p{!95afC&D#K&&ucyZfN8Ca9@V9tgXpzBe*-5Gx3C!CwD#FzY0 z#z;OZLBn~hBoqAr?3d`;If+>}y3{+yfx-W=h)R!upSlktc@09(HXfWUU2DQS5ZiQb zv$;dtv<{99%&_b7X}cX_rSZ)uP7 ziT}>G4#8bU5A;I>4iWg(B2fS9g@HPEJ9FJdV!!jhlidegFu9M|8c*lv8m$nvrB>`} z$R=GgU-_?vZI`-ruHFD}Uk~nwANTX(;d*~!eVckWt))}RLDBlp+*RoP(OV;hdd26N$hB;`~u4UDA5xn9_RF+5>E zx)?6>j<{z}UJTFApAFA0UZ}352VYrK8?N-@=X*U;{pQ1);q`mXMKw`=fBkMazBw7L zR+su6FZ|#&oV+T4x6hH7ILc%NcR>*fl{CWl-N0bQsk&1kx)0_L9QW&Y+*k7_8J8(1 zhJ`J72`YcFRY|RfsiU#5;T_o6;6)4RAW@zs~cb(I;AQvhuq) z)Q0>DhF67Xa>{#9KIjn~jU-7#T{LLQUF7)J~fxg6~R&=zeJ zOtq&|(Z7wGl%)!k=p7a$$aEr~5+4P}yOi>-1ht!Yh42_iIEOv)VbrAyn*t|oLZDWq z&l%ZrqoU49foozh5B)(MKPDRv7HUp|F%-xp#Yq<^v!0&IC7x4p2g`rG??IoAz1MqZ z?g!o>q6?GdIJgC2GEEt)zM}Mo5AaDJe!9KT3kI+F#A*$VC5;`AV@i#gGp$G>sqw`h ze;+G0gvU|F&3lc7*Wdjxywf{c*!a{cD?83fP%23hkEpyD?*H{qrt^5p`&X}YIa0HT zr!vcqS;p7_aLhHD%D;@8Voi(dh9MZ{m3G?J59a99$r)1$*j?~A$r4|fgZh0yp60*cEZ1} zUz_A7%DPHA=>KpNJy8*M7n@OhYs5kXL)AK?9|i8#2S+&H_~a4LDactEcTdxEzib6vJoW#N2>$=N7^rLaeL%t;>A_&{1$x_7jx@w0 zfr-+Yj*Z4=d|9QdK6wO@TxTYBk-+ULHp z;&dX3OYJo>gct`)=Tjdac@2Ek{7%3hOEk{4R0#EOk4FEk<5Wb}Wr z_g-6aBTKp_QZW_Dq-Lq^UhAy6najC2@7K?IdhgmLD3T(zn*aOF&EY@*k&$Fo&pLAg zkpZ}yc5??3ZsFqb8~xI*^-Bm`;Heu;>tQk1Gp(n=m7P&C*hELv3y!K|x~C}xUgVWu z?lpR2kDEw#xDzbhm)bt_N^7QYd(sd*bwU(gTZOV%o63f-u+bH+yb+}!lux|mYP~sA z>d3C~NfbH?u1$iqG|BR>@2VGkY_kYzpwRW9E5QOrbg2D1z=v*VMQoOu51B*R3gM`b z4i0kjg2aB`N0E+p)ue0v30CTv)Zj=Z*zcg90vxgfH!($}25EF3_>eE_E-(X`_Cgr$ z!biE>s7kpgqy0@HVXD^*j=3GFzS+u{H1)p!{HcN(KPb5IM(yaa+cR}_t#)*#X(nIZ zz1LTZ{%TQO{J~bS1i)BKbYJa`;0SUKq|YNKWPkUo+7I`-=mY|IA z&=m_TA(R$KwIQITh=uO2yfliY#NLCRVp$C~Ect3YW2)~oZpMT$c;Ndn(k2CPKsPpZ z)%-|)W*;hhRr*!w!hEbrjK*}qw~{QQ%~&jBT9=rCIfG16#&rp5NLX{Jp#uJb#P`a~QXnUfgpnsbrDF<~{Bzf~ zP0rCQw$|1fYc20*tp(7ubRj)$pVyU`Mc%};*H+#lM?iE=6b3d&g3}9uO0IH+hu6S* z0Pks{KOqS3*M`S60vSpRPN&>ZXiO)F-Z5EkL!I=Pf>NfWJlfprp6(rWZ;pQGet!K+ z_vZPV?(yMax2rzomVP|UgKh{voF8gb$K(CO?#bbk?%>^i_mB7g&_Z_490(Oj?f10&HNy#{+HyLn?Qp8kcx_W zSV?FC7Fv7F=|jKBCz)UBO>Fw`r+nrSFZP=jU1Xa?G5A+*cg#>bP#tAMYdJ8D009oF zih(r&4oHo&r<{>k_yngDLnlgUcW~qq4x&QPpkMW>9>0PX>hyT=K?qdwbr%(Nx(F6< z&?)LRG(|<}oT7qks@icQGW0)m@w5Wl;hZ2kl*d+E2e=%tjgQJ@I|J*^4`osxi2%o~ z=I+PT0gW3_fP+RR@$NJd)Lv087cO;MDAf88 zFuak}@Z zdr~^9_}>8<1!)eRYl?x*x$920-rIhrmG1*2vUkp)*uzYE;fw`(Ys+0Qd$XUCGNw?!e*?1^EEw&N9)#FLbb2 zmd>gxI_umwEFSAX5ap$=9Jrw@QR2KmY>x~N`^g!C(D|e2B*3w8r)^e6X`4O8hL$bc z)Q%!hh<*Z;G{BS{KD3`G7zylD+H!!ArrqR~_Tx50`zdY6!Ho#W9XxyLpvIAcWcx}( zLm3HdoNJ`U@xT65VB>?@k=EM~92~*3OSK)=;h+$-AA*wv*ABH1DQ4l*FVf4dB^^WN zfGG*TTMSoyeebFcxyNj~V3bzme(vd_gwJsjo`Z{dg^ZdBbUiz`LA#CMk>xQRNBKL= zM`XH+>lfiddL0yvo?)wAA1EM5-1y%zs)Avzg1;rPAS%ijUbs(SVP24NAK!iWv|*0! zK77mfjmDvZFYK!vvuf1NR^k8KXxc(7`faIgE$hnmE$UdAbxpYTz}f@f<$<`L^RCVt znEd#@Y$%g_f#$nR88BBB186a;MW)GPx>fSu!z=k|M(H!f<3E9`=)adHg=JlkPuRW8 zO9;NT;GuhHL7PjtEiBELf+cS1THYV0P$Sq_a&q!^=6B6PpLBU0iD{_P-}4mHm=>B^ z^4j9E76Zq*yecl#Zk8pudwTND^``;}8q=4g->*d-vH(VUaHem#b_wR51qx}7CUzGj_tQ-{ZM~=8i0XvNm&^#CtUj6x6%JCM*}=ddOWL=Z(2ul3onHpNuEt-U@I+JGQwXB6gt5c6V?Uz@)QGU3&K~9SE)udoWLZhrEHixIKh*iFv^%D%KX*vw z3k5Tf%lZa66@?KOPxLE${L-#p&e1EnTdxbEU)ulpvpe|^9a7dg!45j<_>%_dkqMeQ zfM4loGwLM$kAAPip__;DVL#unJdNf`1@w86rlCANSn8sD22sWa=VT+J7g(tH50@P9 zR%UecjNaxJf4TD-{vsHZ!3+m(_75~-<)GVUw;Sy$!{`-8zd#pu$TyPVhd?PqRR@7G zzK@DME+qu|))JSz<+Uf^K1{4saMUtjyW1J}ZKVA`bh}nL3%_B;3xtiTE6XbBfZ>NI zN3_QXY7o$hcB&B@zP}!MyWnTP{LnrBUl~ zROCdB1bLxJnUVvAr4xFm|aOt#nL zFC-~_bZxPulMAvI*Rui5Zx3B1H7#$Bvuzc@Ii+*xz^{uon-B`tKCH|PR4kbnEMY3Oll>&*zaO*r&b$JGda*;RLRfTnBPWxyd?Nu%v6-fZXNR1wzS* z6#k(+NQ@5zB^B3#lLu8U^;bmi9c8>Fu12|NeG=?rY=i`s zsPGHwWB)Si4xI4pLZbZ|T&azicnG49&PO67s}4_B8pnotNr1ws1H4P*j!+$eB63en z5jMLxz5Qm9{JSzmMU{a4n)ZQCDmJI6bo#}c!~T<=Tm4$Sp=en|y^M+zmCaWjZyGw& zP)M^Y3Tt~j)%qHY*kHs6i-N*`!%d)RC=P6BTh|NidSaU~LUmtt6xl@d=%_kCBN+Z8 zXcB=9O;IsSmeSybW8$V`f;pB(0HIgr$2nn7a`P3rs2~=wT{*x_`p|z2evR~l*Y54e$dV+Z(~Z6f*%|= z8j(Re`uzK!`o8nDB#+!ty?S&Dg+PxE+7E#*>hh_A9LU^O;Do@&##8ROpoN;zL_D1F zr}@E^gL^85xw#nzI`6bf*VCsT4dFCJ!^jQc?lfw|haTz(_Ptkqi{^Mf#KSm9eZLR| zcW{Hi27*l5UzMd?fZHQH6FA`D6V}rG_30^VmS}k(`D9@SB^!I9=zgf4*5rE}*NPsH zPMQ+bW7L4J)%}2U%1GInABsq9K-=6jea}5bo3Q(MdnxPG+5>A3{P%bu@6NF{?$*Hd zXZ`OsS@I;!@Xj7%VHr|b$nfV?d!BPM^wSmsUCyvz&w`+&$m@_` zQF_o-lU<=bFDch;1=U2cwOk}1Y5SI;R1uswtN|Z1ajp<|&h`GSPn=80K>hM<0v!Yy zyjzNXVB9|FUOj!)y?*w(J3Q16u#dC>hkjVQ(A4iQ>cf4}vI3VHnL$T)PyN3`1wS5( zZqI$0&F+c%gKuBF?mm8b-<_PFYNt!Rx9Vk@cW?B2Z-uf^^UOFpBh4OP0{MG#1Wk3oF^L$akO zMbP;i7<{)SUpla%sXXeTSc)n`9V32?RHayj103w?{_S>5gQ6@q5LJ~aj6a#gH z_nD%i1G)`Ob2&$s1z&0oEY)3&Lp@9cf=S2635Hxirz(FTe z?L9_ts9}+2bb`vTv#^GF56XY={755zUUx?dZqPtlE|8$DYPk+lbtv2eUFAQVSw3Y4re@nT8L4+UCT4>$R0RkTEheaWmjSp^}#C zEPYEfkv88$nG5|x*=Bt(Vb>B6YNw7l_>|f?+9|m!!KB3R84#mzQbB781Z^J1^|AKA z+5>;T2mD`SQceCF38e1N!F`s-{&-08ieLhmWM%gnHRqwm8GSo!Sr#T@&cU^n}tn>(8^}y{6@(LGy zkMi*p5as!l@P_)@jF8}~(ark7?O^Ayd-nKwcXY@P%e?pNhkW(jj@6(0L;aDD+M$pU z9Q@-apy6P~+n4$gQGp8ead!1S4-6R}zZ`cb7h2~|{S)u>=#D+T%@{pGK!wp8F&*Yn z_u{eok(%1_RUOk$rysjxeXkYJxVT{&xk{nm^?XQo?7l+ZH_Dn<-rao{NoS;;6PvD* z;b)iYBYhtHE@+YEWf>AJ&Y-}H@|$$;D6p}wQ7&8CKm9-diO@O}1n29e-pcs(*8rYC zVZTbtEp36&hByl1RDqNCF~~!o2wl-dwww~hwP3(DN5QDnsn;EFDo}@iT~^144j246 zMTL{92#DqY06+jqL_t(Z-UdTu)z<(-Uht7JyX{IPT;b1y z^45Ids{JaHr$Au8I+p5X+I~8e?${OkEdW;J_)A6=2|XQCI;HNI#+A15X=%a=(dFp`tS+4{D^vine{8Bn;_+KBICic@w<(KvCoW6kn zWuUkF+>I$VYzcaE8S7Zs9d5Lwl%N&Yys_m)96fn7DHpzHNS`Kzd`}(xu3rPG0|?4j zXt^$6_>D5i=R1tpP(UN5-3UgY2%JX3@F4N@A=OpgJw^|lI|bTUfYtYj%G7sgOZg~g zIUM6~)|QD!_R!uYj}g2UFnEK7dx8K5g^4S5Poy=S0b7*eZzO=&w3;89t9 zKPg(Q$SZ3n= z;I<>babZihqzoidF!OA%*q|cXBvZUP$1PcHtSEpWJ z2e1lz8a62)foaQq0#_22LRY+|OzF<#ET`Ijp&45`W*y_nk72WF9*9^)Zqnp+nTkrI zL`}mm&Ra-J;iuRNeq5$~jaz$Q?SY4QAl_>Whi}K>)2RN-09Wtfk{Kv6bNv^S6!4QC zN*z5LS~(Dgs2(~613RYcIi^#y^g=YaR0l}^X)P1pgVwCAh$oZCxKXS1-ctALt_IuKFZk5MD-8R=E7s0mR(?2q~Avni){ zbz_|-PcGiuW9o0CdM>*758%O&`-Oq1+>Rvv3>33!N8>jlQ_4hyj z>^^F0_=Vm>856+)j=;yZMx(RX=p){<=>uMCcT4qIPR_n|pHKA*x^67W9=o$JT0=Xt zY_l_q^lq@m$K&qRlQ-S}`0@YpIv+;|&usIFMy}gC?K^J1U?I>)-YLr9j^GGX?^?s1 ztvPK)O;WPXeqcs?@LEq=uqnF*$t^zu|m~XJ_f)*Z$9v>ADL(UPvuiP8@ zMU%s2u;5}jI%*+I5+`6TWFg3aK{^2$B^^aiAyf(#Psah?I9yesTmWH}X4rZ?l&2Hs zj9McAzj?Rjvk`Upk7r z8j8eLuZ-H*)t0$*Rw*Fs8Jy_`_{tg^??1RhN+*kB`tgcj*hqF#Y59-k&Fd8 zs07k^EigGf?{jM|A@e{Mk?t!&lo9BV6vZSPu6e?8Z=;8{OL22w~t6_X- zpX&Z)WI=qTlRCf#O;JgGrb9s6W+(%JDP0}ZP+K9$vAw^q5gSiET7zj#`wEtEe{nDs z<@ES-_v!aP^!-z1KR3)liwtbg@#h}E2Sw+dbP%-Ipv?&fJx%;h&llPcfieOc2b!YN z{UkaUHm0bAEGah(XYf@JMGFT-T6yc!J(BLj&q_ntw-e>;f38BrsV7oZ-7%~W`fdH_5)W)qi~>N!Ll118(N#r#hxjUE zE>X*#1P@TQGAV@3(L1gIhCc!DJW>m(xIR@ulZhFz3$!V(HGgE`0?(>|hsaq*+u9hq z@d!|T6S=BUYr3@u)*kqaJ>S<^zrBK+6o|+VI2fY!_4NGjIfOC#i$j5ijo%ymH1nI1Z-Yi&Q5yeLu!R2aTz%J6c502XWK| zkiwl{>%fnBB$s6hc%wcWQ)L|7aAU($-eyu)oXMQ?6hP-Lx}hd@he|3CPazNr``{Q@ z)nr8M91JjWAf!%Fp(7Z=+@UeGI^SkehfaU=+vu}-UjD)q6-B31@%rh~0*noPQ8l?pZ+J`ABS@8$-u305ybRl)+tBYhW($Tun z9GeSmo9QVkl0hIKrr5+{sWC-Gs$>)PU!KIT<@m%MEBP+;tD5By_!(KiP85f)*%jrL zeLz$TI~c)omQzuF`!hPDg2k`s8Q`E}PVY_zX5CPD8GOS{BN#)8JS{+F!T&e%KXzmn0f4wOh2H)i_Gb>z39uVOPAhQHB7Ar`SaOQxH#`pe<+tAP2&Hv#`Lc+Kc#THQ8Ql zYZ;M3c^CynaEy^FOhqB65mQkV*ic7btR78quJuNafBjeYN$u!T^ZahFbRRjWA)l3C z8TIKpmBE-RrOpH6{#1)ofGifErA_0neu~O=claWvsL;OHMFl=D*W$95HcBO`PHyiA za3~l-ASsr6Mcy^q;5k6<+iWrHVcE^>6w9_us`LwYzoM*&9KG*N~~xt zLINbrCL$}J1K4Riuv8MDJgZhDKZz2WS4+&y!>+FMX`Mx3TxRTw!aPi+spnKiAw@Dn zkYM}b8}#18N0rp)^GN(XthV?*nvuxa>|Z5kR>2Iy?&Y*#fB)|Ql;x@tZFB5sl?wgCxUa$J%>b+m-Q z#`&aMdTp6dkX9^Z`pc1NMHVshf4QQulUhYn%I%IMs^gB7&9u{j& zpdN5}TQ-DsB>Tz`z28BCFL?ViDVUIjwEm#Tfesz|%zDrcEmJW=5Yq|;Y4BFi#ytkOb+bikr>#{qj}yquSZ zA=jIpR&lDWCpjoGYgo`pqY_vT;&H*?@Y}D~>Xaf$w0MWcpw3CufuPuGdq9RQ*oM8P z4~qmq*W!`9T%p(59pWg1^rBCVOkHQ1l(yZ`lm<-;InfrZF7yu?5TF2oe?eE{W7GwL zC<|s?YUuyQrs_bZ;v>q-fnV|GBLW)=YCK`_P?g=odepgP_lXE>Fcsy4I_*qF(XZb6 zwb@fq!os;?sP6Xkp#vN8*X@Ny z`#7klV2&c4H(r;7jxI4kjWEzoA&m5Rq@c&ai)UI3=Cgu55-3{gm^D4P;i?`hrA2VK zWxEW{b1G#nm!Jw_(;F~21rq;lum$WDzT4MBg@=`J`m&8_bj(=`f_t2H30&pn%Ni}% zs{n`B6yeIqAKgPwe|*zD*K`zb`>XoGE`k`R8nN?9fsL=a=NPTxpoRh)UPr@iD%w)k zD<<1g)t!T4sx0mg-z&mgD(Lv-w?Aaxou|LB)hxC!nuMuU&wu%eqFB$F+9~aX(L=fo ze83kCptK z86nz0i9)1LToVsyr6bC_loSV6BcPK{-BfspfE?AKyHyo2kOHdz_4doct(l=!HCe7C{XHxS0uRF^}CUGe=i{8Sg zU`g+5OWY!oF=3g9o;2T{H=#-WcU;qgklpM4c3msVXp1_pyXY=&{~e6 zAp_Ptc!$*sr(WUIht<1peDyv}Uqq)Jy`K}LAg~c5BhW+-(;BrDUraU6nGE&)Tkp`D z`eBfO0F?Ua)&Y@CEyc;sEUdBd%iEv2AKqx>w$>t~?`%6nySM+Sd-d#9XZC$Hi3OovqIp+_NuipIPS3|PP+CxE`VGRXsvf)*A*g0PS$ zThJCFaK=0mJM{s%L1Z1S|`HW zy(*~j_?4!jsB;PrN35~&x%;X)StklYFxrA)^f3)3f*W*19of@WjnK_chJC(>clxs)3hcIn} zvT}Iz#^+B8KAk%64!3=&^5DPfL(*Yn*k^K`lnZbId=2vpA{n@XDCH>55-{2nK?|a? zJozH6d)aBqPBH=-Og+h<25p6DDLbBu@=QUEpSq(T-+B>R)_5>+RmTnh^zF$Nn!YG?OWTX>gCqFjsb$>1Dg*7tK^KA@3Z!9! z?{x)_I4q*bb8t-Q3IkqrL_0Q1&(~)o1#OY#c3CKq8+Ax|`ZAfe(33U|3V~S`@Ft3S>?Ad(rvyH%_Nd` zfiDp=e1Ow~Q>2N5ZUH}1Hl`iWm&^c`GRHuat&g<_)*krVJdmBER1o(r|8?7g|6+mZ z;eBfSM9BLeik8Zm1z@YENw{6=uBDty z54l>Z95;zO0*ruyailF1xse9ThzbWDbma$VkFqF% z1x8-iEmysmXe{c9 z7g{&(=t=iTYayZ++K(SUYmJVN-51u};K`!*R%|9Hvb(k4y*hf`{l^dg-o1VGrhB4A z|2LWTuC9a#e4t}nwm#b4Q))|toV8=i)me9;omBEU6_g>>jN#k@wgW8=ij>23aI1OPf^h? z&mSdSje*D!BPrbR!Lpm?pNTqUcZvczz#$uB9f+{37-lPLIU?sXRJPsZg$E2}rE}y% zu7^km?d4XD?eeP@cVXVX4mzbCdEpK>(;rxmLycZvf)ke-Mc^QTXqi970+-M*6@*{f zsW%!c3#y)J>I;DlI;BjTU@OxYt&vxCM&%DWnRG^@BS)Df+=U54-N|#w0uBm~@zsL4 z8o6-NePSp-kvTfo?1CZt>705O6)`Zy;H$*ls;!~ZtL0j+B98j-3po#iP^+sepxB{H z9K~^_N?7svfFkJ%&_a89%17N_NT`!u( z!L*IIUjh!!2Qo+_Kwo(_;5wtf&nHnuo1)L9z#ft*OT-?gsHp8QdLwT5669ctnIg0v z;UT|cPaAylbQIOqf!4iv`ZlMc#I7jFCWsMJQGV~fXq^p45P9)XwIxPu3Acvd=QWK;oTVPpDnekG+1(Zl&4 z&e5n-@|*YG7kinB37(cSA-eh{4IW^; zZBsFC zdUt1ohpkWQOX-TvUHxdwRFb2k$1(CkvcX-eA9Hr9K!moBwQa1m!IW-kVk89%iXQB0 zSC%Kwy8S(-qEIfZ;2=#4(Yw1?_ILNXql4!fr5!SB^8Gp;Qmrn z0>2J(7=%nsQRy_r<5W9<*sSXnU;cRKsVLW49hYC(2`~`cV3!XE zIjG0dLG*|NgxXdN#YVD}UpXQ&fOQZU^47GGm5s8eOi_XDY4=7`RLT?^(d;U6c%&<` zu1kQ0Hn4G{pa30nZZ75;5F8P_s0rzkSC3+g@ks=pw7#`1DciC+2aSLdL6Z(VFdu;+ zXc40{XeqQ$#41ffhy6y|%g#G>$IVYPR%y$I6F6V-xQ&{7bh1b&NDRxIqc#Cb>vw$q zS4>gqGX2^88(w<%E<}bXyq@28lv}h4o*Hv>>l~m|TF$+`xs#p?9QGp|QC3_Eg znyKMUHIsw`qb4aH1hlQGa0SDYK3QgzW%V^r_Zi9rx8<4_PQ7BntlUu%M%h|}t+tTY zRCO_F-in?e~Aa6v(C|$FI2S9rhil*MQmD)!5-{%VE zBv%!)0#E<{B9=X`8(!U5JtwI? z7rlT2DK{q6mBYNs8Km zQ}^ZTm+nx32le3n1NXCMTJKQr!-t0l-NhO2`1*nHV7GhxbYFpuJ<&vO_vY$W`0MUe zQ_3&(qosAx=hBBh80EOukL;&s`mtJ5?GFyLk;1Otv&D{8yzj;*!fq@l=lWs( zs0Rl(OeT82s5@xD5b@$(Iz-m2&XEi*P+7=4eoVtsT_Rkd94v+RTID+bq+L<|jKIdZMpfL= z2~{8?e`R+Ep0y!FuaoP*^a*&`bf8J&P~<4<>Ycy zX(3NA*Cx24!-cHOEgU&2mu%#VZS{ydHL@+-5a{?KD|t$~+7Ftt0@pE?k!-;IW{X>Xp5LCLOs%!R0dt$mM}3K1Qt+pk!ae(2=h(ZwvA2+ut{UP3L$MR z%QwYaVXM+rhVM(XZbS7qsBcB2F^Fl8_TTIn7jyxw_ixLb=?B<0M>?%L6%1<#+N94i zW`4k7V8OHm$^bJ@60@=zijs@}pPRyuEsuQwtLF)(iSV6PYSw$OK0aRNg{A zq+60JYN~=M{ub7zmx#R=B{_(?UmO%!x@A)DleDUwUdMeBd&*2boCD5$+f6B}UcXJ- zSo*vaFpF3qSs#i0))HV#${_vM9wAw05;B=fz|d1{c2S@SOpj06EF0IvMw<81{J`%# zSHIIoo<1hMOaJ%`wdt5jp|tZgRjtD$-b!y6k)Y`bR|;-y-kf*uKfUW7?>*MeB^s5n z`Bb~7Fok=wd-7N#{`Ef36cMy-tG`HKg3U(Jzh2F(QX+?05 z(C7;F6ThB)>3)0nn*t`H(T}5Eb3^?Fza^_&w;CXDuHeWAO>Oz(Nc%6e+aWN;*JKvLX>=6%KvS0~ z3fz|gP0wE}vzjbq#lquy;ToMIwyUHQN=MBB0EX(z*;^XwPChz;>X4QB`ynd|VH1$Y z5k^JY`H}5CW-%q+up9SCpk?Wjvduvz009sCS!k4l#*v+ATSu9gqM{BTUw5{VTRMeT zD)+gzd;OyAUO$%(X#_M>RstM!M(ISV1Igl<*%7Us(bl$-&N4*L6suYyWlOoZIw&I9 zY&(l>-aSP(8Phi?%a+!-pc8!Q#VF|@$0!KvR~an5bS7P11B%9xTD;mHN;_L%4Jz7j z;HeSv{>q-%B}8q40GMoyqDYZD%Dw;HmJ}e{(+G$CXU{zCiFSl9*)il)>zSNu%8>m@ z$6CInoRq7Gl1vKF7)H9yIzIsPWx_C{!gl+ba(Gk*IMR8aLRtbbDi@1$)*u*bjPT{5 zr%^w#D~d*JXyMKSl`BSUun4WDqA+6PpT8-%@w*<1Ohuu-6qIscgFu<`s%r-~R94FF zFJ-2z{$>U8aLNiSxKeqQR>dGqz~oZf%ThMhL!n-<-|b23oop%K>Oh?y;vT_-9(|NGrj;KjjZ^%M-t>^}c- z+Fj`V{_09!&D0>6K~NwMcv#|r=`SqSxuvx~9%=24E!jXH5P3{#IsSay{o}WPbicj- zy*s^B-$(AFY-}aVZYwM->Rnk>0{2hsyYMkLxKn=DPFPrgejmP&i+rRqU1W;TaC{FP zXxD>&+ueMr;6{OkK$AEbPnhT=!4O|Aq$Qk}c=z1$$tf_x02c|64vq$3If*QAllxQ( z-viQabASy?%N2ecr7LpXv81y_<WVCa*QJgw7u22)|hR%4|=tXw*0CYf10S%^e z5Hw&a%5#m_U;)uRw((UN5lwqKRhd5hGp3@j&WJ~8IIyA78kYpe7_ku}h$yq`o2%+1 z+EEq+KbADt7N0&T@SqDP5PWmsL=WC*KLjOtSn5$P`urFzQLtB4eO&2E{IQs zu}zO;fm(tdQBP;x9;Z>W(C^ZDkFT;!TnD82ESN5G`VyO$nU}b@R?klsb*W5{DXef0 z%&q7i3i*hoaL!*$uhV^ef#;mxUawPqc3#;Uv-ZH+1JfShe{aR~so|Yjo>98j;<4J( z`G2>8J*>Z1TNp}sQvnt_6Pwp+MkZzvg<$I5zmF$nkFO~Ue#+x3>7EdhCL4(4OR|=r zmC?eJ*4J6YfNCz&s+gl0!c$*fC(0y-9@}zLZ_}8TYC4^~Eq|L1u7sqo^J=W>CG*hb zH}xfN=HvPh=#aC|Ey_sU?X{fWkKY z8OlN+kst(DjgYuH*ZchEi|)$@eLrfwjuX~w6p3Hk^+U84O}x>X7|0X{mT^+MP(Va$ z4>C<978>2{cC@hR)x~x9{`f=pkKg~1XiaJMg1vDOTf_PRXQR9SjZ>S)AA1P>P6> z7VR`W=duPz9g#poZ~@@f(UV#(mBT>+LoyjD7CZ`#kp(0O!&O{SFsVW`ImaUDOHue7 z;o_|(Q?`&7Op!pYsHwPxDJo$2fkvDqTmLd zPy!rRS{K2Khsp=;B$l8CqY1n#N(nmP6E|qCAz?pX6*wlijOd%lX;f^^&lf+Y}rbtd&hio6{ z;Xrr#%*aFpdhU!ow>!%14m!aj$|XBkXGQ@4PtB0OXnVF@JBiSKSi6P57=DWp26|Yi z9IR;)GB8+j{lx;(oZBOELs*MfI?KwnqsM=mGL`ysC*V$myQ;be= zV1x2LgN)G9V zxN+_wv@LJi+42L+d`sJ>@2BW#*QvD!)*e{qfxHKMnz;M&zo6gs61hmcaIfaw8Kn9@ zTX-i}jY)bkm8K>N^W3GSOGR_E_rcQ;{^_=I#fNA>BXCHZ1P1EwKL3@fk(Dl-lRF20 zcsUm(p*wmBAuTEL+PBB)dPut779d%oA+*n}lA4x$A7^ka^3L(`j&JHbEt)RYA`jex z=m2QHYQu=9herx# zoN6k{AD*^yslGqd9#x|CEB9Dn^zpOq_0by#Mc#e>v-{WaKf2@758Va}>&dQs&&Q>H zn#uR{;W^m(#eFWNV<`ms6oG^m9%Kv;Di3UaD1nzF5ds_x`UA}$&gYHa0gk)aMa3c& zAyNcD1gZpfVJM0=Sc3z@kj0nEZaV-W&gAiM2cSPXI7LQbv;YcLOc0hrLCl;ViNNwQ zXP^F5-oQ9{Qww#1wjy=LF0^ncogQRU>0X$#bYAx~pC8p8VQ{R*!mNTaiX&ton~E1Z zU+JWXJ}4VyREEl0IBY1;z`2x#?v}Di7Q7+cDK>1u<~_FBIA+5{*S#EVtSr9brjABIkl82un%p8=%NIO_@vEbNK@mN%`Lo zH=?*DlFME+xyFX4qC9=vJ$e12JNn_Zf*jgaNC6Ly8wJwNK7a0x|D~xYTG-PgHp-|U zcJ#TGk0>_}JC~FDiZbIz`%~q$TeCb83cMcJ9Wbv+qcXt9dMmd~I|*h8{Kt5zbK&3X zp7cGTvU!Aw1AIH(gTb}M6*7tDGrTC0MhaFXPm>wQ|F)a=o zIZT&w6!ViXu5A$$lQ3P)+6zyL#cQi}gl*YV^oT@RFi+dA9s-wALuxENA^OQ>Z`C+DEhDf*uRc{ z?T)pW=i}Wc+8$P$X6!!plo0qBb-}JGXBxqOeSV{T3H76(f*HG;`<6-2W@lrsJ5XPf zY2G)Ie{-jIeBPUR-srob4&KSxSFNvcZh24kj}%Nf>AswO_LUJMl!a|!>DRqHdeyys z@q>dK{AhiswL$EB`S18ln`NkPNS_uRyf@{Kj{;I8A0)v!(4szn9Sk6BA&FGUP>w*D zOPn0+mf{12!t*tQh@c=tLPjcd6a9OyFUn2|>ReM%jONe*Ig5;nIMRhbAH2pFW1JS1 zD^Hmk;4p#{gh7Q4+a`z&gdRKr0Qc|ZD4CZ54n`1gxLYZ*jVmo=a#wayf$#MzRqkUA zZP)O8bzZ&5C=3K_B=bUBvTD|l1!x@mf&!{+Txc7^N!3k3opwjAg_%n{zNu6p zrbGsdtf;anD0%?#3-ksJKwmnmZV>8NUZ1g+2nF+>h%s9h$qCWn3?9-E+)!RP2Y8U@ z&K{bnV{{igeJz=21f~gFgN*v3?4Yp?uOq@I%L-lS@MsLd(98N7m%722{u3Q<)(s(m zpy?_KG+mvm0n;g$TuyP5hc5*25p9LGkOn74SNJ$4P~dE{aq06yY<~z(P)IkgR_0Mg z<)VD55^s^Jy`i~j2`n+{M5PbEsxFv{!Zy2CW!gsgHhdh8!KO3{E6RbrHKTOepZEeo zR~+Ta#r`P-q@gjazvAOf`IBigjMRuxJ`v#9ReYZ#cNw{9Vvfek$jBT(iR>vB?lA1ZULr&4|}Zc8SRt zD3~?qBKYg8cXn}ot zDS9cgm(ixiWodB>1sxz)&)BS`iY8=<3BD;C3@Jub1wQ6Xs^Dp5 z7Tpsj%v1F|t*+Do*1SHo(4NoGX;e?NPOUw#_Q0YC{Qnp*H&EWCapfHEx!@C5o;>BI zODO-A>iar;u)xFrQWM4V#^8zz60(p~3kDgm_Z*?DH_`eEPn(_qaqb39r1Rr+Egn$etq0lYE~pwpS8z2DNL zT$D49S)>g}&(WuP3wl`c2c(D7F8wYlg1obmCf`}3?Iw?McCzw*hb6F4DD*B%Dgqk? z>i1iv!HFJd1UDMIpB+8E)yKU!RLo* z=};=FgJ4veUqKBji(a4u9Oc0qQ&hq*TO?}C4GqNq&<&CfsQ<7>u(&AA5!N_qJq1s?@I%!ScuI=5L-m6sM{P)U@rmnO zX|nUj!15?6CMHAikLw?x@~(^a%2XxR*pU6P&W56@3IGt4*;bHd|LLLD*m&JNdGo>x zqt+1{T0`SZBZEF`+u+ZCD6pZ?PE19~;D#3tRba}&GW#IfSGFrPtWs9lMLDtAg~bQh zO1>lm2?b9sfkEmaM_U&UcbM;aZfIlyBcSp=u5Js$9Xx+l*WO@z1Ed}I&_UL>4`uG- z8{kW&%<1sGH(+Qe@8Qh~PmcS@ldio!glFIgggk_~RT}9mHBvZuyr*vyX2j^!<5I$i zK-r#VjB5+gJcmU#CoSP~GUwo9&FlQy18Wcb)gEZ?XJilW>D0zO>gP)W%GK~TT><+QPN;yflzSp0AayUIP?Kl(qXyqcV;Kt=90gj_?Z|lH|h0?oyq!Ap4+nUKB z+LPSJuH|G&c&e!W}o@9t~F#_R6KS3l_bqTAPccs`_i zU!(J12}j`MMt#A59sgH%bE**{vYTyVDHA&Cp(Oom_q}Pae&MD^-_SH^%9ck>l|&i2 z0UBmOQ$|uMkAK~9RwqM0b6XbZI~9;hbc8{Xo2o~hKlMmV7xFM+`r%Gj$OcN!eF2Vv zZ7Q@)Rp->d%;FN&J#L8Z3-KRCufhtQFFh)Qp*PZGo=4gfn2`fajo3drviWq_PB)Lc zyRT|I%n{O`W$3D@NlF(75j05Ivc+XkCAls_QevAc9MO5t07sPpm6?Iia3xa}A0rx6 z64iDDldy?&l+6JKv!I)=EaZwa^sQ;*iViCcdekl)qV*wF9Axtzi*z8IHbD@%jf&Rn zoDicc5kxm)0Q$?D*7cTxk<+Hh`#=|hIPw~~njeD`-LgoGO6;1k80?c>o@zu;LY;Y9 zMzmAG#giN#~oujXUA;IX80kzqO({jX}0E z&8av#AMZ2fzH3TOaVRP{6rQ6x1rf^~V zl~!}MNI%@;8O0DgrXj@^RMM$`4pOds*s6;UKkGF{Pwcxe*ho?29>KT=LlslV@+a zKmDJP=W%nGrpbSEG6fCAFN%Y1OjCKsh+t3XR2df){O_UMhPt*h?I_KWUd`>}Pv&vX?gE%5%!RPSr` z_0h-33Y#X;o;4smeVkkU2(Mm-77u;ay?y@HBR2N-cSS?rKyvgUJm3Jw)3@p)F;zwz zUuXf+OI>d_G%B8V@)F#DCl30ylt8#Fvk^g1dU2u{AC$X4n&5_jNT(E*QPN~^R>BEd zD0l%0g6zZYUNNB`HG{gQNQMPnjUfb{$u2|m|{**Vd(xN?H<7lA<94}jzv8A)!T~{3QTtw zNpZ+h(fE);cZlq#sMwyel&x^(#Za9>9#sg0MyLyN9Vo#Le6Q-n0kx5p@|Y(ihJG>2 zKcqcHz-cuQe8!i<$xN4PU=kg=OpngOdwJUg-5wfe<>;YEK@`!KeW`{^~e$b-7fBt_NgWdz`~NnGUnkRV7z!&75q;-qHqa z2L&@&4G+5rYT(~oXQKo*XuAYA*cIjI#~-?*pMTO+lzxp3f&gdQzV?$wYn&W^*5b9? z*LtMrequ3OrV-saXrr=FKFZ{8v@z~Gf@%1L64|Gvw8~^E{EN+l1HjM^@JC=nes=o{ zJ?QLqxCgWamGpi7RZ~<1d-n6s+Ll*ANi8zEsb|lw0ysy%{Gz(iHnv|?M~YJOpxxZU zcGU}Y!!tm8aNlY%rf36_JupL=wAJt^M}kJggAyU9ungffwi=&5>v#fzg3m6d&E_=dBtS;X;s=5-k3Esb+!7d;AM#i zGt3Hmh@?`QSy_KCV!5RQSB^<*u&DdHKw2Iqq zPg}{pc>JagAJ`aK5x}uhm(I z6x?X60BFPI4hyD41y5dSci7a4!~OMOcnWl?JV$m|mqQ0rJLJ8RiHi-1z@Lzq)kEMCgKO8zyw zsEAKpE9=wA_PIhx)A1??mfLTTqRhu2lfZg4S*2CNm264&H-2!=>QygXZ41}R7%6-Q z2w#*mya7z|LSuH}LLfOE&_>Gsce!0=g& z3?MjwcT=XV&D`jU9mTkEX$ca9HJNVl0Tc$C?WC~72HBsoiS3ZEO`ZCiZ7sqhdzs!5 z4NZcyW zL$HU1IpfQ;5v6JC4roM~cxX{J%nzT$U1AvJbY;+fO%Mf8(hvhd=uq0x+blDB#Gj*= z?0574S77z)I}LGD9ig6w^qzK3+0=tIf-=;j!q1v}$kan(z<115R9VgCq znwNqc$aLVtrr2nkC1T9*5oItz0P49XYKv6SQTi#yo@AVAOc@Z#A@0{SmMMmmS` zmm)=)7EOC*C~GQrpZqDF?}w$;t)wYfi(i_i_@kmsd!#37@pHQ8;H@nwXOXto)zYui zYY(hF@Hcp%y#bZ>eVI3Bs%vQkOY{hw)V?mm4G76h5if(K0V!xqJBx>~ePd1m6W1V( z!j9!nkOydG!bu6=%1tNn|GVRU-Ibyy{yVH~m9^=I>R!$ZHk(_#CJHhD1_2)i`G1*0A%kP_7W6){!GxcKX-9;2F z$k7?mFTqmYr)3cDzod8p^-HLs5Ey1E!Fj*c+pwTvRdkU^(-+oN><)P-sNvBay0Y!7 ze#W&Nzt`_5_^@>-`{>8#2#%sx>(+S}m9$G@dCx{Vyv&5S(hr9G7^)o^BvT%WsD#Z9 z)AgpR1c4aqlW3--;7cWSV4?h_59@T?$VR=83#xtxXc19L7%Z=Ffuf}S*(A=BDkC?F z8ZJIiiKGJXMX&^;fnPxfQ&cj*Ap;q~Q4C-g3iX&QHAiq@GB#!a06+jqL_t)LK@>+- z8QC~XzEx#srzpljwP8mol@G^-EL?$Oa6A=21Cf)UZHpp23x~3mCzjB_a*B!W zL-%VjMlvF6uUQVdLw~gel7cgJQQH_9LLe)+qPoJ10OgSylAS6yscj_^sfAi|ngku~ zJ&hb-r;*rhRJWr#nslUD*WpIf3~;FJwSj3EY9mc2u7!dLBLh0dVN;gDv|*~nA;}N0 zLg1K~Yt)qWR|j!42O%EEr2W>dR%vCX6eFI`34Zv$g{1qWC1b{>U_?%QU)hc6?n| z)E5s+eLYG}LY84n>1NzWM?-4O0*{=ViEttWBnx7Dl?lLnhfga2rJd z6Y$hqnUb_z=in{beKhysk+lj82kzlAe-9%gfLh2mju&hFKy=D5EV-fC^r zy}W5?;vaw&X;0m!8lYbrrzJP^Fx0FPsv6##IPw|<2`spvfGR#B?mvu@Ai&{s9ihQa zBHQ|IDQG}2V@Dl&I`xK^07rcXwL0E$BM88|E`1QDp7;k>_fceQ{Mnl5Gey6X-eUIG zedqB`O4-UM0tY(lhd-^rCu>_ z#Wt1>VFf|NQ8iZ|*yIKkwo?`}s3K|XX>;%gDrKc?(L0%pWR;en7F?uO`6(sfCFe`X ziF4|w1R$HdTajS8y7R48&Ga6oN6^R^NkFIb@a6Mv_wixiB!RUYuCzt$wRR}+v<~bn z9a!6JyQ6N?hEk$1b@q(EdSp#1fvG-}#w!jG6k?AE07)L*YCADUO;-gFD0XiR8EEvS zO^Y^cCmnF`gKub#x+v+vC2a#(4J{Pp3ykNLABM_fmlq(y-}*8tW}_?~N`P+f>67m0 zryn$R?>NZzY@5Z`G&-yILO zER(&5r0@gfjC)TY(oHE7-WCr|c_$7MEmHX`e(2M^pmwq0MLq`^j7+r#Dtg`${vi$_!*&x4N{Fw1Nb*) zTmfn5H_9+uE90Ryy3j>=e<#qUv&p1%u1OtQGWR{UPxGeE(9_c-yoZ!lIwouh*6Fne z)*iUm1HOfHYPT^Arg(qy)1kQm`(&S*QZjzbEvFty$oqO36ErQJbn>M*<6EMF5FPI* zc??7~@W29+%0UmgupJRX{9Ys zFPAgG)=W**|C>EztW6mI(;dSjpL}kg&J1hVeFKpK1zpc#2wX^@Q|^8b6w*gulf0*& zIIzK*68fqyytDbJep!r@9ZjGM9g=5}dMfA!kDx{@x_P6Hz9xwK;~!iJa)<^xKSN|+ zeOHd+g~b%kjUzPNDv&^mpavtNw{+!Q`uz4xBh^J%V z(nnL8-vq5+@6Fem;{E>9yXuG6&tG?k`v;ynV{Lk`KGzifccS^{`~T`bogO=&vMqlQ z#Bd*1GToP zg~ozbP{SQUI=TpeNu3l{TBQ?FX!8QavI8C2*r)&sWf)8)9m#+@jeS1oc#>f74h7cY zD^gti=3m_}K8v`KOQ(>I;(X zS`2lsHQ~S{0U_q2X(?kHp)rC)IYeK?G>Jl95kYHGU?C$?q&X6}U&84kCqD+8Xxs|XC^V5-JllVzG$OTEOZ%}D+9qWahM`gJ5G(J8S7A$`@ahQ> zwx-SHYWpz-eSFJdp3Ky1k9UDm57+XwNt|p;@il=Bjn?pbFic0$C=I?FYPb0LK+`lB zt#Rb3C{JUWibP&%$C|U_Pu=I=|M2(nd~jNd+3*Cr++`AMJpMoe@UAgyxd5Q{ls{K%Qw;M?~QTw_0de&_z zxS^!dYE#hBt~BN3_DXA`F!hAsCSt|I&^zv_JN~(7k8j=V3T`M`tZ%eG=$gZP%Ge->kSq2+b0_@KvZRca{L&J&=~m!<>9IW_XF;0Cmd0$X zW`S!`c^0upuEy8X-=A94G2&?mto^SyVWe=KU3=jF01qs^kNCf~f)A!>reJfTrf8>N z$v3Q7dPb%Nbw?nO*_lipsWA0W~q%w(9?G-lr{xE2;ED@CS61=B9~;%*asSLu+Vs$4b&C(!2MoXBxq=3*W7lxIFDXe16~k_Tks={g*#A zNI>i0Niu$5dd}e^f+-ss&GEH6yE^I4Z_cAXiAD55bg@3!MZewC*R5X}A6Q-vCpoF4 zlDQb%fDYRm9zgLDE?~lO9%)|A=}43*Dkk;Z9l|hl9&}A60vyou$F;{)b5p#D3Lmr> z>jOTe4Ku(-1S*r+Perv{!Zwpsq;sxR`%OU%4Tn+3$G={C?FUu7I*yN6bWxo}zWeGZ zqT3ztj|yd zoiT{a!~$v0V-A&%&-kc52Z8_jmNYnz9w;4cd>Nf>-LP7S^N~hsJbwMEd#puUnes!2 zlwaCe>)~A6j-F@>)hk8x+!0mB(<2~MxA=)FE~mJJ*5cI6=xQhX)BvZGi0r2)iAR9& zR%uIsTqjDntT2#<%G`}$m>WASD}poPXzULb^CQBCi8E8K=ox_w02no6e>Xm_fei&T zyrnJy4oyd)y|TVWOhq{=BR2Y}D96A4tNZ-hAKitvvZX$}4X&o5xE{Dm^x$<{q(Ngd z5lTj3>7nI3j*&nZ|Y$%QNk( z4H*?F2}a;mv$QcQU6tWG>Aut4m9?u_eMJ_o1sDL1B}Ogq1V?`^y!bKYED_i9O{E$c zQ|N@vlFcG~Hz)h}3fK6Uf{ke&nqMZfx@^7(4^_9OUwh#HFc0J%rn_lyA0-@j)H`j) zH{hq<#)7<+G$UXhd>`|`=6Nv#Oqk0ngI{UKD#m&97M^g@K-wrX$vvW(%O>xA{+{Cp zo-&rv%;Eli-hqb942qk;DH{`)*D=i;d`uVldFpu#oM6-BMao4;@^D=|M)WDTC4|K9 z3h0hcppCFSk%BI05})tj0gaImNbslwy*u+mD2s>Yv=f3Gj}+M0-6pu9YxzFX`{&;F zez&``=S4UPjPQQT_NW(E=iT|0b`!a|^mmg*@o)5w${Kj0i9U*GJsUy!QpULG9Dn^) zfEfL+!U#t%lMg-MxKcVz^S=42K7b&4zkVt-yQt6?W^~E!mUdAo2TKavc!~4!w|v*qjSXK$tAuKW1)L-&t&|Eqg{@~*p5nJ6n$QaB!IUA&`%7xvG` z(|6s6(?2zvLj6J>CblkYjlKyzZN1$a7g!6?$tJ)O6{fs1Grwu@P;aeDJtdyPXSaAG_xop<{*n-Q}wfI=|~j!rraWa>S7(mXxFIH0Yfy!4pXh8 z)VS(jofxDs6i0m_<)PwNU5;A~&e;_2t%lxTY2nbAqOze5d8ZvFG=hWeKNXx|w+X98 zrz-EV@Y5GYJYvHF0niD;FhOmLd_dVr;~UUH0$Dj4K*1Z5^iYu=FdBeh5W+9Y2*d== z1u9tBil|DTAn20TNeERQ2Q>(2L{Nhv{&cw6xq`q3yK@lSV0R9DNZFb4z)m$M@7{a# z28%*^sFiOj89^oSCt6Hui|RbEJW3-U5G8Km3U|~HGV4&$0Gk?}Jg5LU?P8X3KBUu_ z^mT9rxUtukBc$M+v`_#Q5$N_Tly6Gw!rF#9>p=IDN0P{&1d|-pAh;pFGje11ahZzp z%THeCWLxc*y2Hp4=(8;q6`taY`C)yrlT+i3md3wI{zw} z0tt+OV3fnqz_uWnjy7M5Oxv6&eh)%^@^Wo5e$FTlfdde-O6`yIhSuO9M!T`aU8n~( zZAtdsXuGLf1ve<8w|@nW{dy2{4{zuni-Q`d;yJ}cL1a?CQU{=_8G{Njrk<+ZQ+$<* z#Doxv>9*XIcfhlHCMC3HEO58TWLO{?0am3;wn{%y-lVD5N?aiiOSA3j|%nr}-o zEBt%yNWsH0r|NuMIX|h2XHOuKCnc_WZN;uE111qepHINQs5!1`_L^?(f&ZU)puGcC zB;SPPz7{H8y7SH)(WVE={pf6)PW-a#D)jf!qqe?=q2*@dH<3AU7*e4Rzyu$Xy$?}b3=VSv`-Bw zrgMwbi;!u~XfNP;jy{zz)|ZW=7?@a8xBR`6M7Jt?pE zM*#arz*sE-32lFh^!?0WzC z505%ISCEE4$ETBz-SO!MjX;UO2C4{9?CtD#FQ2^W-X8s=9aj#!XQwZ^osW;aP3#?; z8fX&$wuwb7_oy6mn48}EFRMaz6}184Yw1utB7QeG3OYqSnBt=VX z4J)H?R*BXiE&?2MG~}K}lyRKuQ3^pGRr+L9CK+o-)c9b8CM%e>5fKPiLj*Gl2$}S! zEm;+jyQolZ_!pF?w1}UESdYdyVCh)5xJnd>0SrSLJZay61y*R2 zBo=Hf2DNf3f%q~FKJ7pp@TM$)@`z41NIKVQRxB9mphk4CH#{{$ooscm4_-X;R1`*R z>^+HXNAcy=`FVHt@l*HZ4=pJ3;aKZ!Fl9g;Pde3%{86+IpV2vW-C`dm>gpu+@$D}q zRe;7!of6MAebXl`Ji$qt4c?vM)+v>sC{lg^YBRJ}9h@k@+|i1!Gy>x1@mCL;@H?G( zjnrW4B|R9owHwXB^PGzELo6Puy5L~Z)>8#GzWnxk9kJmNGY)7lazlXt-M7>MQCTeA z&~P{hCka^D7qZ8}D^1^UkU&%f0kT}Qqc&~LoJ2?YTztw@5J_9Ig`nnwo@~^;+q`73 zAgCj^+yiPW_K%)F8|;RncI1&4vhS8Xw6$F?qq3ljHl_Pf^=Z|#F-B?-)ZlBwN}94@ zfc{oFya1{RlOeCQ;3F{&JV{3s56?C-o8-N6OuGWZQj!N2VyUbdi@${jQSg1|F)3n6 zkpsnD!~d@NBSj=l@~zzP=0y)M(|URmH+N^_PVX`6)Y=1U4?M^Ny4lL>@aW#xUEaR8 zjfCbs3b?-?$&3i^%dSd~!c}Njp~0qA>89+T+bFI zAG(*%UUp9po_71Y``yltLL!pEv}<<$yU=^<%hOlg@#l}-?;n5b{y6?!gOd4?PVcY! z0gqka|M z`}%KR{;NAaS71YB+}682feykPo>1em$~Y0DwXJVu1_whbW)6obtk4A3T)gB93c^vL zqXZNf)zC~)p_4@Axf8+Mc35E0!iE``@N>V&FB-xoWhem7^pSb%MT`JSr<|nW>YWkn z2$A4_`Y?2K+?k;&)fp0dM6$fPfPU@O6qTOl7ZGO})Dyl#& zOqxfiQHQYr+^I@ru_d$NJLM9NLTjxQJ+4iG>Sk|Q8aRZ4l~mq&W=B8 zcaGmZI_5%aJvgA@U)9xcRWDxW0ec)!kV1_1h4N%Zsq`1HQvRUC&O#S}nSAk(Nhx1L z=5#BAbepe&BT;P`!h0KG4}PC@vNwGi^7J_&3!aPpFMsPMQXoSQQIFct15rNDwKnz@ z*zi;ojo8>fDpOIU>sk-WGfe^d^v^OCrR)&I!kLWR@U#@#x&jprY&a00y3k0bjcwf@ zzF%YXhaIJdP^VRuHyk9OL=;G0lC>T6C8?<_5_BBUQg_vVlr)j180M2sBs|{DUu4QF zYLAS%kj+AI&)uqgw+`6oQEpxkR#{HELn-T~%7W^8l#FP71Y1p8$dsw30Fifou*gs) zQdq*cj^R@@E&V7xW;Ys7#GT?7><5XQ6gX5mNiw4ZT8%4AzX zX5oj%`+wXJR5@BOwK(s%mU1yYt$C4d2x+;N7H-J4&aFML_Q0YCCLRHChvj`;?zeKQ z`C?FkFG~D&NZ!s%7Aco2Z{;kf z3~bUj{hC$SL=&9J4^v4GUt*R>Yk8;0?!ymx7O58@LypKLRbFd3CWw2Q7C8-FP7yru z|25MsN1ui!=zu|6AOtexYd(zB@N^XNz!|rp(ax+Rk?U>bRFs29huzb?=iM)_{;m7@ z^)KCX?P9q5Xx9UU{nFaBxx3rP-O=Nt?wLk)?C$LP9hn_QZkZ0Q_ihI^eC178ZIzcj zAGF(O$H4R5I$xa&QQv8i#&a_4YE78!vyD+bRe((Q7G2ARb9+Q^{C87{MbQL|MrZ;DAYu0v_fuxxpx{ z2BMk!5&vK(4-uSd_&&$u*V;v;Oi|%i^GBMZ@&trLF_h>^9mY*>u?f)9OM8_PiYyw` zVKCaA#sGbtry%6BlL#$`2}`1+TTnw#d}pr8R{bMLYDSF>=Zta=V5IX4j7$=cRL9!( z)5)$QHgq#H4TV65f!uY4&98z^%i%B50~FlCarGApc7na_4k32K#q z*Vh3J{9nEiSR$yz8a9mD;Hn3PqQ11pr}F#JFF(ug+7*SVC=zn55gDfnD6z&yI~9eF zzXKa;=iV7b4=CS*v{U?#wLD?&Vm%Z^t)y4Gili+{12dt5m@E(r3O>x|h<^Z)9-J*) zC+Zkt^3G-(?SliN2v%PpDw3>(eCf(dKcjv&wrE2NRjM4^bN*&q@|7~BkRppqf~wpe z7dAqW-^3N;&6@%mSxGi|E%Xgrcy0h_IBi&ypQCF?YBFhyy%pXjlBR{V>1Ft3a$6b8 z(#zy0pY)^~rs^=s?z`S&Zfi-ZTM(IumUuukgAkuF1)Zj(xV|AGEU;rqJ?%i@gEFai z$kc0UZ5-09b88Q*J+RaRy19n;Xy(d)Gj6V=;h$z?%fFz!2b0I>I2FDIvRZON)X2bT zIVih%4w-XTVj%O z`8B8~cqTxtq#oU;TKq%PLn-94WJ63VcQvZuKpONRP3K6o;s)tObiPY2Vxk`e`%nvz z9v33)9mlSXYk>$VxKp(!4I zZT5mslv*Z|&-mEar4m3C2Kv*?-SvIW{F&*!GAlD^av`@$a7fHp{6mxqQ+WHMNmy*idy1tw21ShiRx<3_F|~qR+A+Ebql3WE46eYgq(U z?JB1#6QwkR8HmWcxHQ#H(sK`S3fk+qx{*fE;+Li z92hv-yCTy>d+E~hdm+|pH4}$61nkQMbfIe2z%7I8 zT#`b$+*U511Q7F&`2|C81q=n}&=Sgx=|mF&o)^7HM_EpH;+9I@VhPFQ65^;7OsRr) z{akJ0K5Y>?wKfn|P9!^%(xJpm8D?y}`rrQ1`y=KM+UqxXlie}63RTSDH zoJw+PaHI6B8F_;3+~E3CU(N37>rppP-DPy|QGa2|7)}klX4S)W{c6u(ha+zij89dH zWiO`t2f+pDQ91$?5!Pvo;>+Vd zhmOI@DqIzIKpmWfx8yXY^>WNr`60gXO%tQo;%l%1Bul{yJX%0{%ulr(o+}+A#}aJB zKcpXO;GqV-tOooWBoY2yEKw^LUVu6Cbk>96BcP*x-PGL9d4A9y=IOcDzlQ_j(Bziv zEx@6YO!xWRa7JkklW`ao{U9ij-FbRyck$qgF~tJM0%(-B2R)K1eLw2X3}4h+hxJrR zt+->BQ_wZqmYCA6fRkti3t zo0r-{??%tA?=IfUxpBF`VU^t^5&zqFLx?Fu`b8xnz(3>e_?$^a{ztpl03uvG>0!0DsOKrw@(*0~?as|6kKW8NaG zpsr~2_|FOR_ET{H5j6!(onZqNMZ+mrAql}CiW=-oRA^A`K+BDTMuvp35out&-QHfJ zqCF(^MGD@IvqS|Z8uCIj6D}=Z`80^(-my9@@~{xQJ5S^6yQ?(Sun^&?VVzLzFw_1wwdu<-785_A2g&7$a zueB8A-?bD)Gd8G8_GDp$NAL5HOH#_QK|Ok)i*m}C?L~GB^bP818ZzI&SdWZORmn*) zrK8|n#9JY4NNHQ>U~UqOx5NPy-C+Pgg)W4qO4qTuDO>b@-9c36B2e8&{l{z#W(ghZ zCV2kprR)6FKWZt8rn$vT8GRK+H_VGSnz8Y3|1QVIAGLJqogEoB`l1L<4KLYXuM@RT zb8M88A9Qvp;-D~m(z*XxLhRCUb?aYF~PTH6o zIyUK;^gH9q2ueVxeB`#y2a!R9-Z^)|=itxDmmSo6_T$eH6D_x8=;w+0np|H}RjqEO z21SqEQ`RjsfD*XFD3KM_RBPqN`&#sM=y&rJ@7Idc{*1ns)CV~aHSqUQ1H;u?_gsVO zw{U**$b3F~_zAB>%t`;-DzFS%Ho(~X*b42M$X&Q)^%H1`-7Th!E$UH3wbKaulJux! zOdl@O7JVM;flAhecT7($3+?!xTBclTe0a86Wxt>W1u%UUW3sLD(hku(x{iVo_dPeX z3`5VJ^{Lkv+FydrLvd`Jot@et!K{n-?=Ln#u@~Ok_u8T1O0yVFHcy{C+5F}=uQo4V z>Qk(G&ObSQw0W*E)%V|gzj^ci_2#Fyf6~z5W^=7)*w7bF6$Z5o`l}9In|=eM=Lp@0 zIy~#y0N}dg7>|1<0*(%DU2#j&3Wi&~UjS*Ep9t&qxS|5+z`TWf$tZZib^s#25i_ZojD+}Uiy{V!g zI-l@>pFEA(*ac>cU_7upP+=m{zv+>#x4Y;t>uiY%GdKuG*P`HUu``&00QA8bAyO&d zplG;ell7`R@R_-65HaNhL6UaL*2Mc2Ur|X%g00j#AH@v9ukSu;iONsr^AeSpd__gx zEA4-9u2~8>oKnMR8jU`u!`iEw!J#f;X(ib*qmK-RNp#s|YL$tKs$y3RPIOUX$~O%u zVhS8`QIDMqvCbtZ97UO-LOF11DE4$IrE9+jW^u&VIkY4~pB=5QqCE28l4jIbQGWhq z^G-81-s|fq*B4sipjjIJIttDWoE)O@fD*$gR~}Ku!SJPI)=AWEtDqSrWoU%GzEGEx z6w$$ef(kMu!IL%V3|=_mNf;WV@j_7gOq`h#+>C)vbxs~f0`*Qi;OLM5X3nq#=;a^& zprt7PWDd@AL|}L(f_iqQ5Idl z?#nnh)bD-q$idMz4Aurh@|uWIErQuTMGUpuiG+e@s!<+vi_XqKUlY>fSr`^0O67yu ze99RSvC@U}3=?H<_`(ZLjhK;fgcCzIQ)!YU^*4Kyh3Ebw=Z2lE2%~(KfoD?(c^$r2 zlnyJ!fC>SKa~8~~fv~`ni#hpCR>Di>ePZzreY-fnL}JNUjE7=P@vDd++hq_q2PLTU zuNr2Gn=UDtp$<`7?5){gI>6-OvyRFn-^d;&)o(=?J*S!5TKEz@$A3;fZ226{2cCx- zc&LG>0e%OUuF5IQSG+FbRnoe^$7%&O6*FSLa=I%T)dqA1>7?C;Lt*9+v47J>xn0zK zQcG&r_@PDP{%{|uNSo3ht|wTd78o{vjj$$V4UYBexVc48?r*h_S9Ejf^ZdO2<~q0Y zC80EQ3a+!lmR#~vSkQ2u!^aDhF6uFnE?TbJpeGSWcXH?ju$HGhfAT_`hrZaH>ZLg2 zr%yN6n-3p8ZvLbf;y-9yiDQH94H*1iuw*~q^1b@zg&tMq;L!0x{Lh~~_XefjJ^|;3 zpKQ|xJ!6(FcY0x8J(`-R@gnJhV|A}3p6r8?DAJM<4B|P`i+fE_`1G^O%Cp)NIXL{X zTM#}I3lD!EKE2sodE?J`mhlT~!8N5tdvJW%{PO;%&7a=>VCTm3CogTYHTrp5DU}1q z&l`Ou=BE#T-28N*m$UlHiqwEiEiJ1=841BWXnD^ZlDk4Hs6RmsI4^4i`Ea7ji0rT5|-Y zZ7@b);KAMMg)yGI9XKf}K+${<8iak+3vB6!bgA!^6 zhttlJ!W?xb2}cr)RFgv(to8~WZh7l0e8vk=7HI*~LAKGSfRV(AQqRTV3<(`|0jno^ zK+Q}I+RaN*#75K8!l@yf zu$O*sXT`PX*?&XsJN+(eshkP0q8@|=jc zlHds=Iv@+@foS9(zzaole0fME8<)3c}gDvD++$VmYyGaTM(7W*$6BfZtEY7}~`rRQMw$$S0!-R4x! z^v|B_ODY<3u^A_yi?y87C!1?M({~4obTNj`nH#nqc`9UkmX`wD0`&|VRmbR7ggiGZ z=8qu&xEResu3Qe1p<*eF-VrJle@`Ag_tG0|IXgPn3=Rb7xWO61mqd{UNc2+2GMv{R ze%YM-{4drA`dsCK-!#f!=_ToFEk*f{H~+Hv4}CG^%}2c$=eb-sq;a6bvC}%K7=dxZ z04OMvof`%2gSAc$ZiLT8k#B**q0Iu7wiQ{qYSjJ}6%ojrL!&ZiV~I)&8UXtZssULL z8DNo?9Q8Ci6xM&k(C_+#1R`O&0*h;Oj~q@-2gLWN9#W z`bESzx!To zAP1Y;1IL3M8(NC;=3oEKOHnT0zq13wdu(W#iuX35?$tiw*yyun=ySe@*}#=@fRp2b zje{scZJs0p!Y&+Nnu4WmOnno7tm$fYx1Pu+PUF7w`I2< z2Y5!lFj42SIFV%MHZ|Hxn5Z&6-U4p{I_;Qhc8$Q5^noH*xC8t(WGpIBL6BWxZ@GRg zL)H}+hB$xk%UU2qdPlN^>>wFYyGnUaQKQ)X>0ed&e)SI&4>j=DTm!y*+BK93ev_8M z{33@uNmn>RN|Uebz_X;{aM+4Q$#0mJKNZ^Vrl+BZ7D2py?yaCL*aRx&^3sa(jL$WUgf){0{q z#}K|?$RQmgRis3&;DseW4MAt;L~;4~QxX)P=%pH817XPtV60{jjSm+W-fu(u-}_k_ zeeq1KnH_R)eEfK+86L4sgC6_+Onu6J9GW@(=vqz^J=+#N4i(0yVY4j7Bq<`Q5;>_; zRB4=fUd@yc`D`r+br7{9EdRC}JWEvGX^DzbyzJC4@dkZEnV!$@IVfnC8P}C%6kB1 zouNuYF*TghG8u;m1N4n%Ww3vQ!|yi!iVB}WwX;K;jQT4oLb8v@}TtjcbhO02z49m;0*|{Eq#KUnr&6AavaO)b)wCa9-p7dv2m_gjt#9bJdyJc zueBXyQie?z>K}ZK=%eh85VrWLz((o!Jtwh_WR9=Vf^lIF!Hx=TxGj9%|t4yaxPK=jX+CU)Ftkyu!9BXh3pHpWYL;r~@$YtWfL+Zz)PZ-2zHS z#gaIEh60dkDqhpN6TcI=CUFaXA1U{t?S<`dYP|?IyQiks_K&kb3r#f%Vi-Y|zKSIqnCe6pG}R*rLKM;J*~VD@4h1_Ek{@aq2sSb@p(ns`&S(Bo0Ikwd%{>1y7UwlT0i2%qL1sXk=H+V)5F9YF zH^vF$0sXN=g#h;b%^Nv5NQEPVo5vCRg$VZJ+dtZw!4jSCpJ}f~IXK>3zR_$A92@`7 z=9iCJu5+s|ut@)n2H-r~Qx;n)k=^Rhg-%kyV~jvb?}&XKPr=zBm=~lC)Dk0(G{Lkk zlv0t2o1$9C25Qa!*dbzyJG2_m;Y~P6NjO35Y*3L(c&Ny;g-;;VDx|&dDVKUpooSe8 z3@XcT=FLbqyv!&FTFDB>Q&KAu-KD;wVuMg3!~B_8IXD<-Zsg#2E*KqA6OQzTTGMm% zw%UD2c|z#OG77m4l2l>PVesTumQcR$$8A$W6gN3r`z^UQ=9yHKq zAbZA)jo*H!<9p56U`<$fBtOa1c%gcE_eU*7(K<3_xp-d(4XE3|S`ME+xF#(dDW?u! zgqa>;?x+-nqlL;~Bm51qn4nnGu)r{YNG|gDrZJTixq+kPYzr`{!LD1f%Gp7{j17Hm zmCvhcK<*nHGdX;tfg=ym(p#_f?dNwt{_L;zP{%iya=88YgJx~~hd;B+@*kXtu@og| z%d{CA>Th^m$2zA1UH8~TI-*FRy9z~07LE7RSiCYhVlvB zOjqX{j=LR0G2W>1)=fZCmPo0D53)ca9Ne?iD%PlJ5!G zQUpKNEh0jA*-~*p_wzNnUnjA{;+aD3t%p<3n16-r%$BUz8FZ$1ik;?Wq+8iTsCU!c z2V1lix2oUM&6?R}Y}^fOIm3lSn=Vj^9%y1_1k-Hf=4OS|ky2^KMN`STY{aRt2>kVI z?N7GqS^1Neb9`ct!L-+31ktlEOHVE?^bE~-!16T4(o~L9y%0R-8Jg#6#-u#2O0s85 zD9Lh{IAUyee9Oy4+$1`rBu25=s5GKeub(PBUP=Hm2Rt_D!B|IMqC$!IvwqMDln;l) z=98F(0{=u`I62Yh6Pd+vdVIclE*Pqq>2Z0hmxs#WsNjrcZ}sebr5B6WIt2PeYan+o6mH zPG%5?ZNsEUo0fLfn9^Dp?8JY{r|SqOayDg*!Mnh4QWUL`(q~7#p8~T57*ylnz_CG_t4^4X6pjtb zts~na+9b+kP%EAi80LHf&I(eNPw`w}`$DOwN0BZTF2ZRTkPT7P;Kn{#@VSgJFAb_& zFGcZ;4KBr)X~5@K{drT7)0;7cje?jF#L|xUKfQLnebiEvPw(H#;r3enOdU-Hpgp3_ zbu)3@tKL=j)HOGVa2R1Pdi!hg{e!(P1Wwl#*|!dONeg)oX$C{`Ke4)y|)Qm2&r*<0p zYa;&}oAcK!)`RSa8u&G9U~-Qbe+P6)bK9Td7c3bs{Uu`XOYj9z_XCyCR6usLWiG79 zD=z%VGshLp7sX3L{iL5#M+3e{(LI@$>{<{}WLeFPVldk%(G_WTp`==xS) zF1)(feAED*F%Z`kFZlQZ$_KspzWk_{YYa4XgJ4z$pOSt4{ORWT%V+u&tX_2LxtVA7 zaFFPkRx)rP@U@ZS+jMg1`Sw^&5!?niO4yXMo&qtmRre%Q@WRG3HbwFCz2bm?4;nvi z4g;zSmZ!HL9J#bLOL-U@gc&(4TUcL2aVP9*u z#@nmco68%$=*$@#UXr4EB#$2R>%0{JRrx%a$7+_P6fikv*)2H$CNvUkbpTt)OxaO5 z)le-Pp<+B8Ts%gz*FX-8SR>Dx9>ZDV!+=EtBX_KnSt3g%;Hb`^^KGw)+#!WUKMd{! zA*d|{0W39kI1sy3R1uX*m~b7kSO!3N_S-wnWYA}881PHOnbIh-$r|MG8B{*u zSu&9m{X|k3{Yf#Juq8T?*lg$%tV0p&ZVi?GZ+>Q-457ow@d>!RlGdP+ZC`SYWwGoM2E7iVPC}{+Y6IsaP%Q{Q4r-f7Twgkc*bavXfRkmps` z4L9=}&E&Y#te;2R0KIFpW&vr>n&aR9ZgZpchgZ7MU%l72tl8Ve*MkLG?$9LGTc_4O z*q$~OP6kAO;}fwM)Piicb1mwkmqi4~XmX;Ad65d<1Vmz)=r#%6=etsDZzw8u0ay1!|Xye-($@ zPFC{9*H)cjT;Kuw&2kuJ(a_e^hIiqaj_6YY4BWXt73EL`SLbaTk z^7b65u~J6vPF2utNMY9EWe(pX*H{EEtESwU8wQ9LA@EuWmQ+k-E>Wz(g0uLybrxoc ze-~~_ze-vCi}LG#tixrVWoT#<4s?jh?z&FjF>%c{8HkWQKs%;WLpRA$X%m2cE7Jy^ zK|R>lGd3@_*?{v#%P`(uyx+Y0@K!_J@AV!~3#B#2eJbb1Z}e=A&JWt^;6_W$*_VSE z8{dAXud95cr5@UhpDOc9a&01dd3EWf9PpoLm;6UMv_ppu=oHJr5sn5iiIKnf0w#sz zKS!@+e`YD83EG*wT_xr*nH>xh0*Lg9kdK=;d_^VO_WY5a>Dfp`_XB2dJd*_eK4^)` zm1bjj9FKn0xnF)N{^`Sy9`;||Tm14+E2cPDw6;~ z8O;NB+Nwq-DS)d-HDp+QVT5;%NHGwxCQFbE3CI3BtVe`$xjI&fQ}qC6DLXyB_N zCmPH}gNrjA1lJnGA%_V>(qe(+@$@;vn|Xn;-=;ujI#C%OWI$VR8OR|?0iH~@*a-_W zNu8qmXD$>w1ErlClI>+ea&j;u1?LS;o5xyu!2Ss=iMUdk*n@=6yIN(RzzvEMpNY!3wt&i{8>PNBEJQ^gKj+EIBWdvYA@a<`Ne2 z)UO8qZEp>?HO>h+Hdr#Dn;+JBYMM6HVZa_id@|@t^{a~3>Yz`%y z8?JBlG5WL9X)J~Ep~!!z6RcInn2tUH-u>Eq5bB^D{tJFK=e)g?{n#YO2TNC2vT}3v zQGH+DR9zojBe5(+pHu!;4l_B(*kDw~+xBpj$YzQ6bCXTcN&JM?e8YeAG6ONcj61^oQkxT(97N1?>EQBLRML)ca0sr!R&@zn5b)6F zNaO!te}hZCab^%t@R}oSvdLZ)%;MlHDwKtWztDG$uinu_SR0}2RdY88%MYON01l}5 z8O#=&%VTQ6ip}{vnRx^#O@}W5MwOJbrJE#mqEz04Skk)aD9fq5jW}&$)`q@|p|XF} zauk+E@Yzw%4p2Q%7feMOHxG@s{Hiquujt2g5_P0a0d!{fsmf?!0Fgj$ze!rK#z|`h z!BD)870_52GK%R0nG(|c5Vii-s8Yy~UzRo!P`R#Gb#V1*^>f=U1dkW-2nAmp!|6=_v0@sD@ zAI^6%ieFa1<-&y}1}Rlz#d=cYzUsX-CXdJo~D1|DiaHNfvsRs?tc2mx_U_bQK# z5>vlCWVw|sg%NC;6x-;@Pl%K+)pLP!_{cY>ZFz`_F&PT9z&`=aZEG^u5+!&Ie<*xg z@f0%GGv<369;R(l;=@>yYKyY~Rox5Hiev$`CUu>30kf<0p?kLlyRi!j8)$_y8*bWu zFs;z1ys;cTy^E()`8sZ-1Kc`=*fIuU=pv4^&MAXO6s~*NcwS*#%CjiXzdWb2jO1D~ z5KeTw`}k(_r`JDh{_E*Cnz^Abr#yPaWB=wPLql4uFw{_Pv&<+-j3i*URIh%@fVp@K-ivOMOKJI$n5RYl+G&UqDfv z+0o(AG^uoG5L2f7^VqTJP5ne=TA4EB!+tgt1qd-=AP7L7ci>0%pq^`9ks&g?Oz8HP z4)|h$LVQI92M2>UHHkb`n_8m63v4y0=}HZ;LPjkeKSr|7Jw$PpO&yS43fLg0a~PFr zgd8zYWYz_vr8tq09-J}qXn9JIFygsZXZpZbRFu|&6wh$L!NJBa%;u=ege3!)pZXFN zJblW$sI;2wN8~cuqjj)%5$VLcs2@~MQR2I zT7mN!&&#*({25X^HW*aj$X1n+fi*6_xZou%)5x{)P7J4^@gg|@l57g;!`=xTn^jx< z6P%4?oDGKt68;!sweM zquQlc?SHE^hPRqR=N*pI@qtA0IKe(@ZxChxF*^f0aGbFjFJIz0`h=rL<;H`ke;+8Y z0F#r8kT7SIj+zn+WmSksY2*+YtW(BGR)i57Xo8?C@{s6r@JF%t%#<$?xm|@@wiLtgy$F~92S!WrOzW?3q)fk+hF1VJW4 z(G=4@`r@0%Gg~HT|3Ib!(V0})QeCuVLggp%qWqm46ZY*)@%HDvZJL4uZx7P#;JEgl8;@>J^oi47HvjU&|LfTq zd^YukUVyWw1`nkt-ltAUDP}xiyHh{oLuNoc(borm^Ub&VHiedY|DaFB>iPQQNG}oP z;5cEHgY@0%jXZzszJWDikH(OKGdm@&psJOAJ%y_AOu76NkR?x?^h7{LrTdk3rHw=1 zY+{KD6f8>d_(trW!EyIe)pWOcck|k_P2!GPmPU-tPPslquK?Z0k`_1u?Ryq+FBkOH z${MXELISq}r~)f(d0RvX>tsQ*LOIAXhrLTG4sfQ~OG6Ipat4P6KsY&AqC#IllN}sX zu9#8{{MfJ^IswgDMYTd@sBH6P8h*bq&bsj6QQgHx#i`lHTBn$WAvqdMUTA84$iuGN z+?#?WDr}I-M`EQ5XNfd=iHgX`l~I)2!6vs{711&4W$X|lXSysk>Bd~?X>9T-XfbC| zRFn=!TyR=G!64L2D6Ct~9Sx?hKCstAEqzcssvdA|IA(xOF;th$yhV)(6cKbBZs&n( zm7n4-A+uVL^YtkV{TI3#V$8)?RL z=vk*XW(_^|qb9VT(E+S6HsT3Qqt?aSy1Zl*oH$2*!B+^2i%bLYJlTbqGoC~TB5>> z%u{_P_8DB@^b+*u_C1`#5j;MteB)nGwH8S(Mb;J;k(&p*48kH%NtltvQR#E8g*>!V zI&z~y$y7u}DU%(LLaTO^*IIoCRjwOdgg7`f01AmZ4-O94`~#$TL7OB67Y*Q4b<_sV zZXUhMn!Q8?YW>rso;ksoQE59QM8)uoY4?tCbg)E4Z>8Bxk$@v(@hd9o2@+aNTjt9gBMB%ddEt0U8O9OgI2=r&Fmb?5dyb3eUVC{>%=socSbx4ksJV) z9k@7$;HGyAHI`vuPF*H9b265o5E}+mS|m=EIR$U=fT$!Tn8+UuoTa&DM_QM@gwJSK zv!yusl&$LoNMR@J)(d1o*OpBa&dBVgL2Jt1GGfu91tow6IV%*q!S~?5hgthc@1Zvf zFqOH`E?FC6d8a%h=5x~}QTp#+gwIv_!1quCfB7|#_p$NW2czqXnHRvn-*e!QU*j7{ zzJAhWm}}mX2Q5CHKbS~430WtTdqvVu=)Nd3*7)3jeS+^xkGy?6fnTI5Aoa2psGa=;9!hfC!d^AdtyxggNqANu_3jhq(JO?g>yp>MxvEUbEvW9RGpl&Y#EC zw?f1k8>@!Il3CJ4Ffp{KA`VM6N`WkGh;|ChiNB5kmOP^kQ*H}^KZz1)hD=8kA`@f= zSTIV9aLT@pge1eE;b*2)Djh8p8Zx!RJ3sn%Q-YB zDZZitM#|`fmv7~~5XzgiPi7kr5CH z?fSt+%a>$Wa6+<4s+N~m!MEat5PXd2G2b44LWYTVWrNxp$46Z0|-|H z&`JSrG--5|)A!>lKU|p(QE>)5k?l^Fz(%4(r7MO611gO0g002(p)DCz*vc=&AvW)2 zbNobS89o83JgH;`o`~8=<4U9N%!^QYSIba0Rf@t_CmBe> z>I8JUX<_E{bGkWx%02CdbKKrM!nsh!Pz4)d59x;*_>(b7FLUgIE^`;O^dO;NyQBqfr&U{G0ua9`_4{%kZEN;yDcT}jr&z~e*)HrMWb&bN#j{#h6-hBLJ^Yo{un^Wzl@krxZFVogI>*f>V zEQX7Zwfy8a-pk@8TN`Mo#ipZdM0$JsdmVC?{HPfd%=FOA4dmO=!7L7zhs%~@#t0fR zcuecP9Q(6Pyuhb;DuM0CKms*Y=c+Aa0Cg#RCab6Yq^$Z1L3~B!?aePLQ@~lGaw6N0 zRUhoZaf`ir-hX@Zvz z^$1d=BXWX+006SeDiZ@tedJ3}(s^*n)cdStllXI)B2)`|9gmy<4&J3T9SkKFbwXw~ z35hzyja@bkdTol}G1%;K;4Yc1ObjNOq#=GJ#~ce1+5z_U+ms=U8u5&;sIa$ymZ%(g ziHhW@v61>oUmQUUIn8GZGhxe)guJr6Zj$7yUa<>LSZ_yTp%_Bcrpq@#d$KghB9d9A zNU9^MJ=i?e`)R1->0kSBXhWQ*PxO_P4`NVPQ+a&I5IS|Uf;@GMSf5u;WgJ9XX#veF zgh#9;1H!mqn`}U?4B10m2Q0Qjl+-EdvnW{3w>#wTNE;)k+nO4CKD|EHGPk0Dd zCeRT#V+SCOtbboCFu6|Q3)x<>g$*u4@bM|vN7^uyqTF166gx6>R^MRr(E8#IbjXGG z6H8UJV>HW~s$EK^i;(58y{M^!WSHYvngT~m02itC^Mqi`jD2c^52EG+!U456X=6pT zB6^h{zP&sL5v{6lqwiI6ppLz^?l9xefvR?Bx}x1%R*5Z=M2_$(<_H^Q9G*r(Nn7Vy zqAIR?+i-7&b-Nxg4>j;NQvDf>DS(P>zj)p_D1k03inuB5+5`6u$O>oo0pP-l?{Q0Usn)0$Z1M0|!yw*dRwv8aq0~i&6Fx+Bl31xuJCZ9A3Y? z_>woqlgOJGE+e^c=?&|8lX~s{@c$gMe_DI)gqE(f?6w|DxelpWg zO0;wlZUKbwg{_+({T#2Q_Rw$x`U^qWpPzT7On7#2@F{jsdD&PO=LY**c#~b-BwUti zd}8Rz22wqfViG`vjhS_I7ripg!671xr!UH8t^*uj$CN?zc4D0C#-*7`$5*mdH+f(< z^-gtjK7aLU^Z1!I8d4u&wgyYN*ktrl4l?A>*P4R^Te1vgmmmOTYxWB#)98$f#PQ0> zMIGrU3P!}3PGN4#1^B3I4|*iMcY0{2Id7rrYM9_{cS82rM+b02Pb3_c*(dujDPKr+ z4;NlV#VNDcc2E|_SBn2!n)d10#>cbKu3|3m z40r2o>?msuy9OV5BfU*JC)`1EvWx ztRY$=X(J!!>inH1WKQ!%nd4h3uKeB)cx0jykc5mVgCk4YKVz*)m8NW6t-(1#lV zdYv_Ja+TM>PM}e0iK!Siho)fEB}~Fi-_ld=%0-+xMUyZ>T0kGePYJ_AULN4^eg#$rJ90HlQ zZOSt!rGEWMAHMt|`_EqJvz?l)VOZjDe0ZZsy@B_Iz{N!)I99JBc988O&O{Am>oChU zCs>(CSp+tMhr}lmaX>|1pl&tu;rOvWS*z(@q;U$cA#FcI0}O`&Ga|T1%0}clP&X}E zRwSTD{?Ugn5AzIpQ#N4dYr=)2EMbT0+n&p;d&(j^kLB#}=aFd-%Muy(;@Hsa2+#0f zd5VisWl`)v{b!QM=>*KvBb`XAZrjF zJ$v~=pG1D?&m!}cloQcH$7~b!sCoBaf7CLrKj{lAd;%AT9?PE$RYnteT4%8ey&uFjFNpAWFP(~kxObg9H6{Sv-Q zS(i~XRjZ>+aHCw(g&*N1i5e*WptqRJ_5gZh^#zH*D;sEi%z@IdK3G=v7mCchcuq*c0kQQb>oU#ciV;wzr zpXwz)dvhGi$-x&?pz|}blp+&-XrBKG5J7m;XSPQoKn5rNBO<8~=ZLF(9UdE)7Fm=V zhYs|N6&`7}3JwkeP7ds3HVb={u+4)lVmVD(=G(2w`!jp>nMr$y!*J0WuJfbE|NH;> z&$d@eCLs-&z?=!g;*1FqQh(;H!D6TUg2OP8U6Ozob?}F5&Z$Lw46IzGq>+QO13ott zB9b<_KyhpZGXX`8qGC(9c^b`B#zbD@4d?_%2bI%;0UMzPf8{Xn=VCR`!$8xNlV-`{ zvQKhKhwH(T1xs5^Rs@NIV**m9#K9N!m^9R2s^s z+EIw>mVSf%j(VDugA*;6bi$ykz!h76uths0!ZyMqg0wf2>I3QruK(y4IPGw7oScV~ z9-TAl(PhbG0}%aTOtO)kPJ~@dD zgt*%Zcm>sozEWzYXZ-CLHB?Dv`ludBuTU_vp8)B9KzmEo%C7Ka7xmhJ*#2+F9nhN5DeIjC#fp7~@ z-1m&{_slgHewY=9P_5G#vxwdXj*u}uw|icH+qBz|Z5aXCrrd^X^Y(f-4i+C%UO(?8 zs&$H@Z9;>`=R5_Z5ey0G%?qb2C|cq*H-6TPf; ztg+W)y#QsY3fA$Jl`A0REYC#K*4ULJyW}y+PBcn(t=Re?8*KPw`K+#SJesuaj|F3^G zK~^e-w@W}x zM~qx;zR0|+Mg!N!T4umt*7mcHi0}+XZ<&F@MW{eG8UvlXLbqJ0XjY-X2ZpGc%aA$| z%cjX8`#cN7vo+3SD|=I9|0sEuohk~(&B}D?KtQx?w4JC>9z7LXCon^R2G00|7Ivfw z0bqvUG6a>OuA6L&W~(H(I#b<{#<3wUryU!biso4xz)eRiX-X3fr%X5CkOxLEMiEdr$BPThdBb+tTq%vG(W z6*15(CE;7+9pFQ9OUlH?j;*EvojtcB7kPFlnN&&_(^D(F1}`e>tSz-51JI%DP0l>A zAa_oZdFl-6HOI!BEoh6UVrsiEQ;vcXuZ5u5YOx9VSS6#(eR3y47Bi!SOpl;No(JGV z4g77_K-?F&E9he2IWhiyo!32s8sJjlh!cs0V1fns=YmRJQ$7eP90s*?;_D_dhY##6 zIneGW-%ruxwgfzLzTNl0#=4N0OIjdV(6>yrZC9tW9{%q&)PV*twxG*~yuomzlwVGvNk82}h(I9i*)c zSW_fy;GI}Gxh}G}kW}3i`84m+Cso#s91a=<`;X7zKGy7w_(W`!5W6n#KIn6@%skPv zzseOhst!cL;o2kZS^=gM2NHSnDHh}gO&k9gIJCV;|?WB5y&gSHXsy%zD6iz#tG9gE`h9u}md2NeCS>1tNVa+a-pmVO$r6 zM8{BaJ6MQLDi=koY|9u9P8iwenH)&L6jk$My_J8Yi>ocM!G7i0!T08hHDcN+*g}IR z>=QF)I&k5(jGYRF>Gq4fwIfDyh{|yYXF$!2jhL;0lZ7Qe(aDkLFLqGxY+k}42OdVa zZY_sCl(OA_%q4q72OMg}rYDmyk`Zi>@g>GI0S!KuC&kT`iopg&?6ZLrDe5q;-KtMyHIx&D33AFm;!6s^9(qw*>09}ucsje#@PtA; z7zFdoB+AH)4mrMn(*~bDe%xHXc`L`p51aQt{OB1QENSvm6!m$}*ht4l)dy`BL5|`zU4V_QPBWNnfxtVSi+>+Jz>aQ~uq{rZ963iFQL}BQ|c?1{qouf-C zY-lTrF&AQlp>ZIvq@o=8XXz?S#TF_)@`jOwM?0nS6OH?#tT~=INe$;Fa&Ftp2pJ@H0tmkO{B26X4;e3cs zSj$KjmR9IC0v-S6`6^-1FviN7;ll}1`}kPS7#%o;_`?}&AV918lnFd`D3mwlh>Z+2fMQw*Q(~A25z?uI9xR^I6x<1h6eOm%7i)=O z0p^hl)~6s3Vp1XYnFZ68WbXu*RQ5CDz@<@R%FD%|$i%6Fs6?jZz!%hXIw5aJdw>mt z;YMhnEShWpf3*VLyk&Od(S_*f3bNf}RS9G;`th}5tAJiR$Z9_V&xVk*;0*Z+o=qT| zuJsj_FeGgiyV6F4u^)F8aM^8A^C9D$g`w+RvEZ4ppDrccx{Fd&I19E zC$avNE;7o5fi{vL4hbV&3Zv_=L8$CL(k4T9H*rItJXntH-jv7X!XpbqVjSs2oUmka z-e{=-rDkcU(F)UuyD$rjVX-G;q%2CIWFkau6x~pZ!ga`Hy_V7pVT*) znS^FG5dEZn!6vj<^bh6j&`|%fgF}uDW;xNf-S?!=ZD+l*gS;e-0TktmPI+W$GVn)3~Tk=VNT^&dq!y(dl(-_-NqwuZtx+E}F>Q2>Dqp15p zN4~F-jy77hpyY333sP}W#uNP%N^S~)Iw6Xxmrz@sZYc#{&`fCq>StJ2IlM?+qJowX zG?lhC$cLdp4zo>C*6@tV7$KfMIqOe+o`K+JR^>IW_7_1sF2xRp<9sB7S^L6E2Yfc6 zctm@{m>~ZyKYiSMde6*$@mT(5^YYn?oXPQc^Ud>b0xmwv{lnY;LZbGx_#`KYUO?+5 zDf?RBRB)T5_aMer6iJ#G_ZeQcxTtwT8lg%aInK7-=b#~9e_%Pni6O^}HW0nhhN9lD zPqNrV_3@D$HCnFXifna*o;W-PBj@a(Ka&GQR1vhJ2Mj7613dS-Oq3~ZQjLL@Gho5)7j5pS&JyijmgWg0vsW^l?t*F36L4V=uNd{nX+D62Wg zQ&?zg+DVctP2EVb`OqUc^6Wqm95R*(#TP-yG2-)C5#5=zVl^BCxNB{q_*R2B<1~0b z8P)CWt+w6$3B};CjlbvFkR_)+f8k&j~ZB8wNRZYw8_=S(8`Q@qvdsM@JfEHgazGthU3gABE`Y=Q*B#T8VKt-$LO?^`pq_DH z++2LvT&X|Pr|Bam04JJT&Frz`LVU!v&jwBrJ8*Drup~wFc5IZwGEZ>yI4Ek@sccJV z8kqJrU}Fh>=KlpF+wo`iL>kgm+FQD(EXwD4Wx5DMh(Cr8TyKMf+*mx7_aDC z0q-?r+8BGucM}|FgzJA>@it_p&3rT1j%>Ssj5G(DII{fe>Aysqe)U2>DEPam0bR3k zUH7ZIr^XuKA}GqF7zl@CXX-45nWnC#MTXAWQfu_^7p@UkLdfg z!e!do5ni%BJ1zwwfZTn>Po4`G?Lgvv!_=@=) zXrYsK$|xb%VEo3{vpQ_R#B5xQsqD7%X1t^E=dr$KV)^Mjn3OjQ#I1xR zBQOhhq!YRp0|XHm`&BMY6T-ES6x0DrX0$p%&-;&MbC%wY4ZaoQy0DZ;c_io;&q`63 zYQHk2HDxJ9i_(>SboxL3*FTeuCSkA~Z;%mEMAI84P$Wd$X^=b*NtY;F{@9DO02^etMLhejo7IK1GLmc2QD*v3WW25 zfgHB+r3o)lVM>_Pzy^B`Pt%whLm0MZGiKci<~oj; z`mV$D%Dd(nn0f%prY%}OHGTy~vnlu@1_M);r+A4?sB(FSBJ@sLc7r4&y`wa+rCEw1 zht|>Qxt$ZSZ$>b9(2P?;n{7(5ZKy$g2*w017b(Iq0jZgnNd1z%HM{ zzD$N(_`%N);L{^yLzuLY^0|s%psa1#2?U!XX*IyGzk-vQR}|N?)DI*@>UiXxAf}lr zyTxQ3QBIe@hUg@u^TnF|17`+(0EdQ;w)BMlquDibKDDJNTo1k`xHe=x_Higph;u5C z9W7sq3*b02W7;x`Xf83}BQ`Znqo!+t(x3(SRKj-dV3txY5koQrhL8f2tF-ZC>IjA} zKTi}4;tW5yPy=HoKZo^mcxIYQ#9FaTE+W%Glng;%+#$4NR6MF$gw24ntl0}zF~F(T zS(}ea?~$I%ya($8=|c_tW!Hc%o__uG)Vk4!(1?4yXhxPXT}qZSv;0##e$%>j#5SJ7iz%~wuQ7&CTZ(N|Lo5w%PT5p@)WShAR|*Q1U6w-NCe|l z<|Cc-CWIhq8x1_n&d?u2Qeq{%6vHuJ4q;PGHrLEUgM0qimm;J=&x_m8FJPn;^L%`z zXKQSHq?zmN!EwwOQh%qs^i|!0|9GtzjeL9n_lS7Bfhqf1koJvBgtUV~jTz8mRjvL5 zA6A5AE#1lsN0e7q+YVy?WjSL5(c{h8qbEKdYxX>KgtO!FPBXeU?7N{E-~>A|6xk5+ z)L-a!ta^$1EFFreR~#h9<+2o)0doYr1ED|uqtIFM4nTDT(?Ffo(s+ts8Y9VrE$9eE zZ$yA5=MnCkcraNTQ`jvcgz?!-6+nmVXUV~AiHc``%=I>9rKsX)Xx9UJtA^7C*B7J; zZMgy&W(}x%JxFumfPshzdkM^fV2O$c*w~~2+}Vql?K7IzhuYi+f+Z@5aW{7vS~ohmdL@h70=9V89MZNLooqLNu8;JPShWNc@RT=u9BA?7jHqjW3Xpf46z^O7?62 z3pN`)I(iAUFXXq1y#zrbv7Z?(nCi45@Mm+OQ}DTy zBp?~asAw=Mc`?*FM!L^RV^JwMK)@R5J;ls5xnAZ#@@x8*Xuk+{XaQyh&9Yx)f_umP z1;Vjfe`U}CVh^o>QWiRQbM3V&?JM&RrLST+-fhB!^da+51Aq55P#4?2iuhHoD=9Cz zpLMZMjixAwiQnrfuIIjjqAsj%e*-Ssc}^Ey z>Ft_Gae(RNV$dA$IxFjtoIzed$MGC0JkOFuf^rbzDOG0y0#+E2N>>cd!=Vul4sEu1 ze6~41d7|TK4EnW%!;&P#FT|PpPGE_OKR%JR3H0LWb8Reox;fM5V!wU<-R2M9e%!pj ze7m{0`e0d)wB-F*v+!{O-11zmaS+^ol9v3c>$$C{n+N#1=12!Dpd|~;G|jj$YzVHZ zlZZ_%5m7(LVoW9|hOFVKq|M|DyS|@L|FMj&PqTnT9*RckJc;Ya_@>C86i&33nFF4oJ+UOe|bcaya zL@TdkA(^#eq)d}_pxV{Op;z8JAuc%KW&ioG#OCR@-`E!8f6OH+tQ10*LP(c`ih3gy z!iiXiRzpsgNhfMIDDVAJ*q=qg<;E$7 zT$SZWYrrf69hwcIhBBS`Tp5wgDv3;;g%6T}Fd;=U6u5(w8aZ4w&F)MnqSacc`V)X8 z7|T$i%Npq$=PWTY&p0L65I00cMAYF zz}<36$0Bj5YZq zZKKHq5x-$gZcB_@`_n0|xEG&kETt&+i`+$Fvqf;Iw0uI{C31$_mV78AtD?(mj4jb| zN-aYnS;8#AMseHtZOBMJO?TR!no{(fDLBO-uOk*yy#=-XnJ%c?#;B)-02hKl>N;0HsO-x!LKq#zd;CLxbQL#hB+}BL$!?1 zfbz|U*VautK2lx1dd^#Y?T2{wbo1Sd?>9fZ`F8W>;+M_k&4tHL=#z`X{FZ^7sQ4&= z(g8_$NS8=#21X!`^q4Aa;bZ0sy5it`qFE{&*wKQ<2wJR?V?pUbmkw&k^+sj;P?T^* zK_{6W3pyVXX$L|sOM#F<2eqfcAxl#N?-)T26=${)Z6QLWL_=?ISe?>(;C*dYbRBDOGATpHDZdGd_DQiUJtag$hGX_}{1xR8n z6u>~}z&9z4F{wN_y=B&fY&foc7W5UE$r2mkKFJvpOH?dfeKi}SqEyi|NvHQQ!4|bE zOHqy=pKG#4d=&+|eCwd@ygx?lvEhDzm1CV`nrd;~5F3jtq3;5j2TcWOc+DMBV;6Oeq@~ki4EF)8H*JA$A|o%_mfhG!ru8jU7meD-`9q z*&5iEO$|^rm}EJvIK--D%2A=%&I@86T!*A_Y`{=)@*p9ENCWB0t7Q{jy0F2Zgp(hb zBU@)N(QM0MB48jH7>dO4059qWf`Fp{6&GQ}Yw*OEz9Mh(8em9-tUVG3`a_a3V9SO> z_&$@S8DB~p;4=k|Y{=Rzue8FoM%jMbS7_#Zl_#z=@)tu9?Im2~UIgwF)yv%P0ETtM zqTC1ILk;{@)&RfJ?MfW4j(LY4akG*xp@7{7J`?Henv6x-B<2SV)0cEF@r}hCcn~ul zQ&tG;m|_mk^=F9Bk<{DlaE)_H^iD%S3SKcZExnzvhz2$-xuwITi>IRQ$ZZHXpgl}; zzylJz(;Dtvm8xzM)Cr~0D-6j3MZBx_gQ@H89Hqw;R<4vgnP?QpUzWi?tOEr6`0NeK z_}BP>f{W+$dj5{3AILyeaE7L!E-Oo_aS&TpolB#^#vrTNZu&uX>M1VMrp>m;L-kmS z;#nGc5syQIjWqf4iDztRlg-~f|HI}VUi{PM+b7?7=>}F^>BTL3RCu;@n&*-X_S|U9 ze)?FmK6LmioN^wp1m)w$3(ekmzj=G9nf7vx@QGLs&&uaHyglSqs$z=TO5=KV*%MZE z7ZkWH;=myhe=NDdc_T*#OKzOAM$LRXEERJSq}pzNL3zw+*xHRqiZy zz_|Eqwq&?*0jB`vr_O~mUg)NR0XY0RDMi~^j<*#R?&Iq>mgfsXxKn*a1;@sTzM?{h z3R^5dUs3rG`cl7VZ&(qME!esY@(I0!NhUyfMpU)4J!DA^2UC~sSSo^ZNFmN)mKI3) z0`d|S2C5WN5ld7aX&(;kKUW)AzRK}d2VYUiIS}c*5DRPtLfKFl9_sSL7a|Z9#E1hJb;#Ly+p81~y-x_uAU$d&3DQAd0!%x528Z;9 z0Rj#@(5!~gYqXOCpOb|j;1gK|uUJTu9Ab5KDIHPS`Kn4!nU-k%NHfYI@#Ibx+Duck0dtAw1Fo%meD9qg)ktQS0Z0dMOV64 zXdAfEH^@q&2v^W^@S$xn=UdEoVOSCMG!S<50Njz>Ta*h;rm|fbHjrocC5eZ^wxw-DkTr!)a}BSD z8Dpw3cNhWPRLV5ho}nf(QvL*{jP;4%B4%<1Ee*p%m*FtZWDd{del~-`Rk~?Bd!Sgad;nv4Mgg_+nkRmsL6yX5S z(A?(}IXIZH@!iwk+qv=T{5Kk~t7y!mnS;nO?Gj+cSA$C@3jaWAvkuQdjG z`~L0b&AZo|=jYG02cn!NI7~E-dH&?(=IPlp>npc|?VygaN%c=j!bHxI$y5}xwUKCB zI?H2zOU&|B$tYboHjeGsIN6*XpKqS2ob0)Q(*wOYT0Y%fX()NAv86UV1&Acuhk+bK zsx!?Dw}V6D!4@aX&Xr(RS+M>tmZg{*qx~i|iltlxF)(q35%HWhkvmrbzg!4LJ2VACkw zTT*ltinWXZUN9xBM>rf#REtCD5CHf<4oA>5Vr3Chq0+>ewv({I8#wG$y|`)ydaEdr zjw@42w&O-oI!#gq8tI|b(HNA4%=r;mJR@vLUeV3sCi6fzWTUDX+s^EouANLP$elLq zL=FPKqCp3Vnh_0Vf{8Gr0ho03zq)57@z;&2@2ARc)0e3kia8OqB;Tc9sDcUOL*}6d z{;F$W?Dj@%TXR{yuot*6D^Z}e)d9-$FT(gUqHFOz4ZPWG4p~K z$2dE*SVfVsoo9*$EJx-*tc<>LaaiTKdSNs)w+$I<|3u#2=KLfhv!| zO4@$u^SUp-mnFZ!)gf7`qIy41`)L|3#>=;Fbs0=U zw2TQ*rbEkHn0cd4Y$uiN!?B~odu(X74s|GqLq<+x){t*y8Gt6qz7$22Tscc%Jkm{p zSsS#Cd7UOJuC@mUwu)R4vU-Xp$T< z$vcu7Bocpe$(gC-%DFFBp{Y0>HV+u(a3H0t8~^}707*naRBdEGayzCn)Hf4?uXMW1 zNoE;I2OJea2M_;PQa~`I$2_p@(Bv2ur3pzR?~oqryu_JduD_5=@gQ%4UW4zF4Nk!% zYy3g7bzUT21a7e|0(>Fw;0KlFF7odM?vTC6yQ|(78L}QbE;Pn@VtR>IOm|+gY=ljC zyZHUpF)-G(Kd|pjF4?dITNhLXU8i`!Jk-EnWexb+ir*4$UH#@XTy%#NY>k`?Z3(vo zThp_Xb_F?t$BTVUN5V(8J)}b4k9xnneH8Hov5)Gj;O|+qAipQmJq(>np5_oU?Ymtt zk1F-3MY^7KkPuSh?99BcCd%?HQh@4911_`)VA#BkI$=xM*p>@6%M~wRqmUCc6^9*- zpy2}^R#d4fH^w0i$h@E<)tFBGjyiW(mief7F_G6H(SX9{q)B}|voZn;+OUEgM+9-H zk#KCVOyjqnvGI?a=ch02(0~$&jI$WKK7IUr^X<95EU2;LFP8w0Kykl6Za&_`?0ufY z{Rgp*8;ylN=?jHk@+};^k8);%jm2h2IkgZO$`M-0M`~)Zz(TpavYo$r5ge&LV}tn` zF>8Z3Hc5SU@@n(^V-Bs|PTzRHC2LQ*J!XZ?`Uh2;xPAkQGFB`VOEkVGXZOXP{udKhJw zI|7C}U~dbq9O)IvaJK>2qCse+Idg=X!V8eWkZo|GBVNIe;3~$HQMzTTYp9zfW7Ia3 zQN(dhNll=Loh~>rFyavwxu~0>2@8@cGMq>K9jerna+;eEE@v9Unu0a>vbZU<37>-h zl5*~QpKGV;=5t2ppH+U^;XYy3ZnNyFq(RM)TqLK~pc(0a6?~CtM!(*#J9o)Hq?MLv z3)Uq*q#kPEZ?gvI>%LBusO#mc_y2tt_@AR{TS5OE%694KwcAs>=nHp0>f+iTFUVa0 zbvaWcO{*u;LvG7mM5d;aw$KeQrJLp;@-yn>SEaEU8uZQb1LaP?%G1m zXvqfQAZI4fl{JO+98ZnY93lfcz^gFQ70^5vd?E!2^MDb-6RDC-F75#ZF~FG)ncCCg zrrkP~t|?t;>cqw-knIF;Zs5@P=Ip!8%hPW)+d|7w6zU@lF;-x9JhMBVZ(csuh)WO#rhoxk0?ea~900c(eEPe;9vmYG2Y@P8 zakvMpLZ`4D^FvIQ*;-R@KB!aSVFU1}M8lv>8kfu+15&Ud!)H!X?lAIsqa z>h46#L*#k8ey1^f(c8V zpZO~)^0vwTr@zrxU_{95lMjFTIWAca};7c14=M-jiMz+V$w52%TibLVFe2+ zL};&@uW5t|XvizdiD>HBL2)H3;d}tO1x0z40A#+vr2s`X2xKNx`vR>w=j>h3kV>M1 zXa_bY%bq!Wo*#q{$vM@|{2spE4m+jnN4oE=C~IxGAjSshw2NkeEkLF3(E3~zv;(m$ z+h(_ZwZa}m(Jxm>z8ZMh%!&A_tpb*LYx#or0?d_^gt_F-`H*_3fxo&MPatmrKgW|_quqBe!uFC!y5ta#az~nC5sdm+4y%Y|@(FaxJIzf9f0azv*HiaX zw9Hq~Sydh=y0{@WqfYj^KL6aJpMHtwZV@f@H}!AO`2DL%Kci6hHV*Q!N>gu&>Uh zO9G0(^Sp!7JXQ0@T!&MA`W2@J&H@JH(A4?Je?bbFMuep^tLQx#8llo8v75H7NkVL8y+SBo#OY z_c`IIl)w!57@3XajjF0~hMY+c>>zPY9{H|3Y{e)7cH2>-a;Z$7#R1s0Mx}{vD4R>y zY#xPLnrIcn643-m4TM%*0wq)O16~}&sZuy!EV8Sr8{yb?>pd%EuV$Zoy3qQmAAc5? zW^m{@(-IZ$f~#dJXD^;@o_+V-=0%iHd5$}-_ z6x2hYtjx{;{8(J_NZP4E`IrHuK$UXo;B=u+K^iw^r6S|VC)*|h#onKS#&;P7$htI% zg+hb?1>3B+;3hIA#OkJ(H4wl&!wqR8Z;1)na%QAerIM|>oiJF5>a?3GG<7@;<_BcC zgK#CwS>rlLHZOlF%b^+4O)_;>a063r6+8(|)|i$O$+v`Bg58sRTg1%3Z302|(X1Jv zKA7z_sTy?b6S-x_nvNu_{Lp-EBU}rg|6P{C~(-cmAI}Ws4_% z75XKR1rugO1!df81TR_ z&ZRw-N&vFj=NO6x$o&6Cr6&`Yb2d%6_$P!iRW9!)NlPY(kQd--t`LKFHtCQ7EXo!? z3J!f1t^e1Av`J{NjQ;%}kl$95(4t#C)&$g-MHGpN30M)IgdKg@fF1FPJA9Wq2;u$3 zo8j@^-NV{dEUvo)UWzEm(mr(9}GXAzQpp&1*WZr*#=#?|e)Dlg*>Id+hI)q|JR$WI41 zG)sd((4NwR-J{{z;fvwt*FOzEzWQM}Iz05Ojcx7qNLRpo_IKR5)nIYZVA>xBTtY-& zDa|IzeEAvc1e{I9?fGjYnc!CuOES=~gj>NB!brsFhSZ?5G!zems}AS#11{VM8)0tC zX?c+y$XZCOz=cV2jdi&d*6^s()i2EBL9RL=L4j=zlJUtw4wigKHiwRk{RB8@ z^qqr4&ER;hQ&v6`fFatRuAoQWZ^(NuAn45IRe+9+HL`Yu-I0{$r zcucMvX?Po1ul2tec#6*ddSq5XD=%JT?b;Lk_luN0)Yxl$$Q!Td}GQBMSWn6AXE zA_q0dQ&BRK(Y6)oMjpE<$1P|mQg(ZgZSuxVZqPyFOZw5}0Eb|jND@H}*`|8l)|4w| zpxoZ*q2(Ep0dP9FVG$uGzl1|(xnnN=L|?fR>VyGhR#MnC+7bK>MP`-K&*C{n28i&TY?6(c)Ti{0tMV?= z-A%E`I)}Kce^s7oD^^64*@UJcFWG6mcNAYHvJrTmUD4Kne=?V$$#;2LQ9 z=Q_=EWC5$adhN5-$(|FI1nY1~vq_p}ZTc|ZN2X{IzijF~Njo_1dNf|pT3 z86_E(`4QejkUrufAEJV;zOF;aRZWclpj`AVY#J?V@p|u!V#p}>6qRX!M{9}&RcWQG z8U4SVat-6{w*2Y+|N8UgaD4f3`0eypRhwqRYdmnM{WTcO6O7=Ci6dUmuFr-K7jK96 z3T|9#Ls2efK`={#paDxZ9`9)u!(*L{a&ROVKN>4|a&7NCZ0sud!>o;?-N(bLXRn7}UjO32#xwm;zPl@182d_2^Z=j27>~N677o0w?E@d(eqT<;96lFqA*6%QgsLy8lzgm6bDTC zpeh9teO#)&E+j)2v?E{AjtFRz3MtJ6SQib|VlqJBL2v4`GTj)|GR9Y`Zl^4P4h^U^ zXeJ=Q(v7Rrm;v!MSRPz*|yMj+Ko~S zn~MWj@2sTIz{TbUYkU2zUrtg%3}Fa5e5qaKPlV0tmif^P@ejnUX(s ziHf*R-zrdZuD*yd1U0UC5EV4w8PAzT3O2ZIZC4D0$ueb&U{2ULdT-OsAO+O{0kRzg z4tt$3Ne08*xuNrtA-+&hq&n6YN!IhANFsr!Tltki7q&o(WyXtcgmL-mf%x?_bJmt zcU<2??Nnvw{}kDl*BNuCCq)o4zVJc?pCTfX8Jq4jrSJP`O)IHUb9WmdU_s{sI91N3 z#l0Svg7{9W7yGC|1YR?A_Kp1J2#9E*@qhV1~m}fGdn4rIr9kD_n`Z?Qh`TW z0~_syC&8_VhRV-k;*z$-lTZ8<-qJ(Er-e=FDVs(_(DDDtU02FmM_TlnWR`v%^IZnC zcpKdU?#zFQ=HOZ8T*ef0Ot7A+#YUC2-<3;lwOmUy0l$lK#E-d@*hBhK(k!Q&(zpKh z`B8QwtMt{LQuqcA&K!mXZ@|cBZUR?v!~_nAFL2qPi&0V3|H;B-KIkWeg+ArX6^pga zvzYn|NK8NiD)~~1mZU1K3&Oiu{xRCqmI5ru&}kv>&wtnV6=S5&!>fazbX=+qkB|Vy zg%>wxp4~xUgQH;CrohLbviyU}!Km_Skr~_j))~ zkc0g)(1q^H&*#IZ>-P$5XsoBe#w}xAs|lq!Uh+}73T}804>l0hXHVm2!@8f?0DJl+uCtKitu}K(}!9SDg28|LA zFiS;24_N<7MFmYkM{U)+6-B9J8%U)ZGhp*78c4WWl#LQdFr&GZkA{|}Z669)gS`xr ztE`f-pUP()HQphvXmP?odt{&Rn^rK{l6Kh+%Ibhj<6y>uH`dg29ro8?b!8=oA?C;Mmo490DG!-r@;oV2yolue1R5R@B%y0&ES9RY{7uP5?@te_1CBwdD!s zzzl)0v5M*iOQ3KQ&jXwC`hxdi&*I%&*j5@4>9LPq6>z=>?Q(b6>VjE_mkCtPY0cqH&Bp8c-#*9elnhV!_C7B9V zOAw+gQE}a}@sA$9$SdU(9+q{18gq!AWTfhy5*rJQxwfK1-qM4OEbTNSmIsgn?e1^NZ>iU9gB%B%hr ziUq*3g3Uy2Lba@;8U+d0R9yO%IPggetX@;bicn?){T;bvtrJ&i?;@JPrzuMLYMWAw zt|ehrR=pT$7OJAKR0%Z`h%9D0Y8dfT*j%~VE4Pdp&x{VqR}lY&1gymXi}!?;2YJcjU=M7YU!rf}bM$ktmS--%j%N-x z^IV#YTgA;W&MDsE__Paj4mX`J9KhHeazQNwLlMC(pw|sKnf6S{3S;q66#d%)PWUF z|GA8R5~Fd3Un{Jq2pxr+FK&zgxMTqT_;@@*v5pRcw7H|JdCN8i7&0tS+0>eVw2yLV zHOEL-*he*CPy{wK9?*b)TQe63a2#KK(l5OKtsgfv$KpUUIJEgDvot;`sKIiSbDb)} zc!Q6RL{-p$Ajjd}(eUc&tKsKYKYQN^)sk%9R$${~cz^tM_;~hiIJ-JkaDg9AWA;Xj zFK{eBX<=~^u&(siexjQ;xNT=`XdKEi6a_j6L}+`@vJ~ajaPa8R@Q^fk#>Pj@);Q4y zqL(@dgF2vHT<54onQr*RIF=3vjB7K0Pq3@WBj%kvr3Vc!wXlo;omZ;=0 zu?{-6Ry2ReDI`V;QbeYxhi>i(5ej~lj?FEN}KcC_^6S# zG_dEFeeYwTfiZbD>AX5UcaUT6NYjoK7;r$K45A&>@P-%RE-#&^^wJbOLLV0hD~w{? z6Io#3#SF9#Hf?{QYsiVkxK7PVooJx0cm!WjpZRivPygtAr#eN22aKQq7kh9p_}@9u zZnx@)iUMe}PVHs!=DnAwTocexC*vW2W&06p(cq=ds3&B{$Nm}p2A{As@h|K&ZX^NcE+P)Szk-CC!Y3K6yX1J##p);){T~kX! z3Y&F5XYMzFH9pT8lh%F{W%m%$*frrwi&}FtpWh{v69zjxb9~_PYUzR9MW1|Dl*9r~ z<)kd&Nj67Qc6ms;*8uS4p~+{c0afnA719d- zE}2dlg|E;mR=(3SA2o~Nr=9mc?*qDazyF$s?VLqCGY^*@b`y z*R}LAV}scmEJeB09viWNXejh|G}Wp0W>$(f5p`aA>@Pk`gO;Q`K72B~c=mjF{`ASP zyT?8Ox5Jgj#wRDLr;o?O@v(w98aF-CxOqnt0Cs4QbkaE#8K6wbckjU?u?cM)gbwN@b43~0FQMtLia-aZ1@sn8r-egok zoEy(}u&`=s$Q+BSISHD(Frrx41~^~~1 zl^~!^F>Q<|zc7u!PD(8-sez!8UFlsD6U1orNVdq|l1${ZDCN`i3AiuP{!98zxn4&v zY)vVR*;HJu*G(CU?t76}C9{=Bv z;VZoTe(3rxT2l(ox1Jh`Ut&yE_KyJB3CKC zly6DH0(_-1R%^MptuLk2tir&~(F$N&pZX3H1{1LQ!EZYd|ep{`7WjO&<8F%wKlr~X$~^NU z90>srX_QA_q7-+JCo2GIG>024FTe(CszO-=1(QvbZho}&!W8}IiHF*OLw~o!$E$Zf zN#x_zW8<&2G#?whzeWjcu*b%c0vj)%zV;H07f)U)0KgC78th+Q4-^1TO z{B=0J(lM(JY-s$bKeRFy;@vN6Y0St*8yJ>{uTlR@T%a9dIf?=g3TZ@;LrYP%blj@~ z8_##O6lF(CQ54ufI%72g8y~OVmB0pjY}A2TsW9oHoy&)Dq4(R649^HrV1wl^yX-5X z6H$&1kA^*ss|ak|D3HT!jW@r)AKty!k+K&TlEZj2WH`9t7Zg=-R6I?j90R}rxY3-a zqreTk;FPmK7xRA&(~Jb<0JS_J6z+Ro$Z45fmIf){CCinh3fWQ=i!*iY1~mK<&adz4 z0AYuQUN+GJ1ahI3JZl(KsnQt%>{~|-MRt%5ku0?;LVK7rT2cnpD07`+U0P_-HX<}M zx}4Q51oetbDZHdG=K6eLMlW@0Qr(?FcGO$=9JKkcP{+Om_RbNSa^=c_Sf6VJIBvCv z$fw`mc%O==>c|J%3UDYeK%j;rW1m}}9>a@Mod%PVG-L@VYrJnJiB1goA z4ysDF*|4muu+O#ww@=t#;L??p5)!u0-hwF9wdWN7xuwmy&a!0jlo!7`yo zno&c*#vj@4Q(}}7b=^JEnas}=c~p9`^ADYf^R{S|CD@OR_-V^lIDK;-~U{H=5o zcq4r_n?gZW8FQ4#&8s0ya05!Jb!lg1SD11Y!PrTEo4wWwnC18;z$J6OiSUV#iHlhz zZ7=^HB0%C>+`9bKl~!(>A7#WhI^Rk2_@Hd%uA>{}er>MR*vbN)7A(=U^bh%m8u;Ed z(DuoqtNcHgJ$2TC?7kvep=$w2mqJ?H9BgXNlzwfwQI>L`Z@_7+#Y|)+Sp_G+c2;Ow zy;1sQT&tqfomI>-By@96N4~(CqB!-Z1k%ip7OX-NH)6(I;@Wf-y9#OPBj3|ZZC#~r zAw6$jYU%JduvwcUB#*7#^^ul{NvZo%-2*z@xx~E*!F<6b$#Wy)Y=aq%HzG*hb;w1-;6OxBhGeG z4!zv@U?CrZ4*14JFoS(Im|>9veFqUVR*F+lwza9|(eV1|4_b=yCmn;z2`h|qwZCBr zZ2a}@e>$*na`jP%O=zhG!3~XlS(f5GH>@?RaQzj#(4QWYhPC8GrLnXusC#Tgctb%4 zPDR;$p}a~3Wyu03nxRI}lk!NHgzyizLt9EywSBnE)=0E>=L+>2K&W2(z`-2<*t z1rp$#_6WIWY_nuQmEl@|9-C-bPPOm=yx7>v zQa;52Zu1i|``O}=)|NYZ5EEq7R6$UOEuNxL1+D_}8RW+jhajf$ynKbAjoYe-fw*%| z1^hRRn=?xDnwAqzDqOJ-5tQjG*)L+g!DtT@QNM!e%f%~YO521}mN=Yg`p~fRVc62q zMe=Moc%o<@Q;GK3tn;y0Uuj>B>*1sJv0%TEYXS<=8le*mQT_%U{F6=IgG1m_SIQZO zYMUJ<8XN#aqryrykkfl%#Ce33%Nv3c;J9HqiUJw5O$lDb4HP~34aY2C5@Prie?oLKliD?UW zv-T7XujPz&)R-sRi-iq&C#Qv{JuR#w?h6}|jZ8BPGoX?F&Ky2}p@gqsU1t{7G%c}? z^xNU??aRFxzMZYvUNXTgSDFut_{?4Rh0n=oVash4|J$0j*wym2`PQa3-_qPSpDOPs zFS+ik?ScNG2EJtttoB3RSEko0B%b?K?vZnjXGZkM@JHl|QM0y5KkyZw3|!*)Ba99m zUuVIrXPX&c6qBsCVIJE<`_j~a_1K62Srn@E%B$ zmVvxyfoEQJ$0)|52j{UEf8u#bPx9$e84&I&iT85ZEB*WxXqP5@l}2Q~kc~dV8+F)o;jd&Y)NMfvgN&ziCE^YBckqwHy{%UI)5V~uym?}op>{p;}8 zxBoS~Ir-;s&TB zOL+)v_#g(xO<3-B(GZh?m;nK;fejq@K+Dd{Vg``psf&=IEM*D+L#T66G@X4hwsf4r zTZKT7^cG^`HMb%l6TH;LUI-i=v!x2)qtUXUbQX0S0vp=zpdNQ?jJfO*xoXxV53avb zF;E$v`c)cb6o8Zg$#b=vaC=9Z@xTlSeU5a@Eiyjq6rYnfZ{2>_MOUfIWSC-wzO)x*l0}2^ z<%YUwTydfrxPgM#i?5T(C6P!KhwrGUY^e?)KI#jD08f2L;SmnIKWigY9ey<2s2|ez z*j3{ZeUc4$v`l5^`7`@}B$#Sf^+mZX!9w&G|AR?ThOe*5P_7_bWAiHQBW?(mxT6 z|DUbpFtTC0n|SP;LA%Db`^nuvbxV?ZJ0)!)319-=mRms&`G*?#BWqx;AGZBgw&ed4 z^*?m%gHLq)n-c*^drd^PsM-8|=;!n=@lWB&JArpa7jz>{`dOpDFR-)e4pqyh-a|^) z-l4FS8mZvi7`y!y`Rxu`D1?xy+E_{ z!-IO|9N^G9`byz{aQ3Ps88e{dM?1zyIGl{#AQhXxjT{&Dgls z7==&lw^4kT*8D*{EwcGpC{=*i*1#xoNq8UStpPB-l>%m-!C5<|XTIHU& z(C5Yy2-O6fXlOa%-~|B;8XLjnA1Sc0_xSO!r}ako>4#WRt81+{^648A;?7DtWn0;> z(4~8^h8$Nq95&Imc~LLXu24mxw5|Z@lc+@mK9+c(7m#3N^2729%}C%2#=e>Vksdk? z`*l>&txiTcdHY`UpNA)}UuXu09uk&w=;1lgDJoQ%WfGy&hh-@QbqG{wb&A?nUsKAH z7t_tCI3pbUY-PwW~49B={WQzT$FIt>eMXA66QR1mgN52i1^J5Zs`UwyiY{My!x zl>zcu=le6N= zI$~tsg`d%XA5amWW@XxnXkp}Alcv@g-!f0~Nxw`Nn4nAQQOF$MlxmJWC_mJ|_o)GY^{cP8-POHuyoKVHrX{XsWgjm9_*Hd4-y!Trmq`|=6L5)Z$Gd?h{D=7SAxkJoMC;<&d9W2MNJpIr}ume)*rnVg5 z&^`WyFieo|e2yC<(AxEK@hrk~*Ixr3_KxvM$(ff-M&Gcc>W~ zub=)n{OL7IQJ%Se*o>6G#>bP7!{0Pp{pIR<`;}n(d&A4GwR#F$axHFkqWm4-8VHYX}|@4jb%Bq5`6}$YnlW%vHoO zu95^zfHt~!3@Vf|gH-)fnF^6}WyS`}Qi2HknKhuGhK}I8)$yA&Y>F%9*sg?i_RIni z4d)CJkioi@JdMI%7+R#;Zb3~$stmI~(yumLLQu+sVB4@vUN%1Iuau3(P*v)vk8j^&efJ{AqZ zD4r*R9^`lRFz)FHU)p_F?YXZ_Rd=-cD9cs|#MN0K!qdl9UByggoDWa%D6dMA0MZfN zLhqOV7BZ^FO@xWY4p*iz18OWqZpHBx3ekxu8hgsEazno&tB^0jTWPa2v%r??e@^~e zn!2&9jS0VdJz>eWy0S;DlbJYZGe7Dt5?@fK_QHW!Zvd3<3hXo~Bc@K>-W7 zzJ=7Uld;TgOhwmB7{19Jmd??2GUjnTw3L+S)pX`6@7ThS%TlB;WG?cR%7f7|eAd9( zdo7fp#?H3(vfJXb8$k|sQYb@hQV&VwqnDy+RG^=H^&x@9)`(a(~6t$M~ojF z?vO7G`opIf%}~gy8y$5{xA;M~S|5Hfu0aQk{44pIf)UJv?;wCE=+gi6UE^71Y7o@f z+tw)|dryX+U;LtvmZBWJ81}TMA7gR?8}Cov4gdY_Z^K{T{nzmJ|OSam6pWQ@FniFjp~xSY}{3Q9BS6a;Uk^UqV~Zn0!bWK+ntJ{ zJr|iRgD%&D{Pk=cje`_;P+$W;6~M9ow0qV^q>Pv73-H?CO9gFMtw3Btjk&`t|HlWz z)2EL$cG}YN6wRcU>F_Zw+u6}TYnK6*W(IIwYs1GY{gTXJ?3Ry+>kg;%4lhrU!BmVs zt{)mxy`%mr?w%6W<8cLiy0TM{&%qhhSox|zDDr}$T$!a308uFf1=QGEI~w>i?LROQ zDxD5&=yVHGHEeJdTi}l@xq5}%T2w}m)SKY8Or9DoL`}BnE5sm-9O)A@1~$SU2KQSf zuyLRz8c%-waoB(Q)UyCEf!P|IJi_Nnvp3Z8(Q%#1DSU4~)lnQCop#$Ga0JytJmeq| z6`j*`ZcF)6m~$*znmBwE4v_>g)bCXf1Z&hb>Zo*5f-_v(>Py$!nDh2h`)V+=LBS0= zGBX;O+3-k5-X6Vri2(0OaUxvUV)k+qQbD^|q5`NmqX&QhQ9D})L89*b$$n@hMWPc3 z%T;dj(@?|$0*|S>ieT7R-c7!SGstdh7N)w$?oC=z-Rnr&f z3{K_A3oz(At|}|(O6tbwC?^CI=a}bU4eMPZ8`3VjXixNJ#7;dP`M*@@C}Ai_Ez^uoj+lK^<7~{?qg4Wf+t+o@1cvWf+0@6fV`p$N<0*;nH7Mq?v3(cIQ4BbvE@%&oGFh2QoW+-n5o zkLFS9|1t*r3{vvBAZ@SF6P&{4@-OnfhcCwRed6yFMm^Gv?2-mmbFcogzk~+*`s~VY zWbO@0H(ow@6-(RY0{cv`6otUXfByde49Az6v7uQLHzlx9_u43d zjhxjIZ;1F@z5=K27T2*7%fnjvK|KmWuwh?IQVzBc9e8ozL3K(%<%B&p^tsR;8xCry zP8p@>CVqxv;&)&J-{d17{L~m|fA_#`&kx?$YBw6SUA96%<4ivib0P{sj6K=2qnYkU zI>h1Tg~rAzpXx|hF3$$x2l9j0uZNu@O)b%q2MoQ|ugIKUaiQZXZ!}oOFr1`IxEF1! zVl*ffxFA$rR1ggs8Nd7l5F#$TBOPlJ3I0Vtupo5_COV4?z<4)5YHCK#D+s`h6LkW$ zx@h@erlM5W%+`p&h77iPDjJZT(20? zTwPIxQVpU2(_w=Pg7m=@bX3`F(!h-}1TJ3=QOEcuCiwL*C`utenNcG;0x${!*lyyd z@aec$0#L`lsiz5#&j$+Tc!`SoBC}R}NgR9^@|8Xr#$b>>GKN0tJ72(QyXv`gW8t91 zG{Hnq3A%)XsaHB@JamC=D5NY)=$CD3n=z1vB;aU?#IrgG7-EpFSX-y|rVr5W?h`0N zZph|tYBC{8nN6-4hV*HUy~yrWG%|Wbm?$}bsX<>8nvD$3`mvk^F##_36(t?kg-X)c z-y>Fe5;A4E3!ZpVx0{q#{lN5T%e9YnK)Aojg}Rliv!N@jkOtr@Z2JmU>#qE*Cl$D0 z?^l2k-Kr`L)@rQi#i~ncf3vu`6M|)PYzeEf`%W_UnI!xQ7h6`_Rg{jp8wvqa<%lm& zcF6Wqfsto<+yjO8;sLt#)2YS~QPcHEWe_gp9dWz7evHY8BT* z`qC(~&H1go1(bZ@C2_mYkf`@8*GH?n&5x}*LR%XOG!x#Do##eg@45V`kCt4N894i| z>@h)LLjetDF6?a|l$jekF+=-1Jl%h;*$mILB;wEaxrV;LbEo` zZcelmMq_U3Qg!XIwkkU6CT4wP0B7%!_E;pCqp|z5!{@`#+9dUn{616r{r=&1%}O~h zd}vh=O5=p{i}T^r@rmlvf{6$>tWZ2j-$d&4mLF7i93hmV(n{03w z5y^}s^$}gV@lVm7T#BOg8-kbNE`be2?;P0BujLvITVp)N#gfLLRAZt|Zec{BY`Dq# zqg&<+$YpNL;MEQ8L9IHt8e*GuL#p#_)&1^cog4g0fsG%37!EbC=O|IG&x%-GsBur< zz8y~Ay&tX=#ky6y@bSQ!zybbxz&QF!{Jns&*oPfNMzp;Y*q#V*xoT`T1m!ZZg|(F|7M7N?!Omv09znwADxRfmfC~x|oN49ge5ufd zOzSHol-FHT`i6E1wFGZiYumT5mHdjQD-;H7qHl&1HooF=FcH8Y7d~f@fS^Wv2n^&570+J4Uk&sLZ0zqGD0uT!%TWI0&$GwRbt=lf zUhVb6wwAlI6y;aV*!Y`*8*fg2*FG1g4lsC+4ffg4AInf;uMLd}85hd;7z2*pg+Hb7 zY8wKz<@G+6BPc_#3ES}v-#PlYRnSTwHb(vNKbEuC;m+<+L_i2;NU8>tnkqtYgMqjH7-X^xf@%NTR8aE>y0X|4f!!Thg2070 z(BXKwYImu>bc5(>vzE+NSWQs?u1MIhi;p)CWe*CXN5SAmRA57+esy}Lwv@nz?7=23 zC^4g&tGu~KWu}|GH9jTYAPYz<5~DK6QsDSN15ob{Y-qLy`CZMTIehs-vlo6G zSdv0}U>eJWF3(OiyWux|-sp(bQ|%=Zo1{9Rp?0c)4H)#XOQSgftH7FRDHih3$`>U$ zLxTbXTH3j|UwIwppg$4VP$ZJCZJ8%J1sGKEw|W>9sd3d%55vx9e^GJr%ej`?+{jlL zwpCaAEKyP5jXs%6R0!mFiHc;=!qtVzh6zg2&5p1x4*_7`6D0C)(@{#$($ z%Hjp^K%Rp86g=_anHglK!iUnUC=1L*0B!EQZG^|?V3+-}&jB4O$iUF=XGmFJz`07= zemx~isHp@&-`$}AUkob3%45+{Zd@qppl`Clz5-~ZuEQl+XV@kTFv_=%v@NvxR1$U4 z*GS@~Y093H6RPt&rsG^8UjcOFn-%Vskv>Ix&(VkP^ZG{ac*y6erhUm&>%?|q=4=u{ z$GL&HMhl)*pc26(jhd9QJOzsww??aBZ~LB|mILSV7_R1HtZZ-ai2#QF zYT%~iGq91fHav?#zTzV@7$Tsfz{dVlFGV4^@#@KIFSB5r!X6vPXF3(-{ojWF_w9cS z#}^-bGKrU>XiUY-jo3hxzy{+iJdVHy4HSRH6T4Y@Xh{MS{uQc{2xb~QBnU-dBkPQr z8`=Z%vnCX{euKqb2T`gI{ump@DJaZPk5oEtHwShUy{X@+ z;i7O_Dp|~Q1jDFj<+l`Bs>@I`YeTJSzaHtR*T+A+8lL{7zy`}QJYz$3sDbZ=Hq#`q zar#!vQP^0M0d)D)!7-zH*ZY{3S{qM0Ara^LHnVXGbYdt|D$4D!_I=U zf9g|g@ah>gd`TdS_EB39bkajeKXQP_8;x?hv#LRM@lamL-{aqYxBRDCqH?Id=p`y@ zd!M2r#nk_;`s9T^d`aOGZmGK}uP}!>TK+{J77Oimtw`*RW*CvT zAcR>UO1RR(VnG@d$Z{{h?qTZ)N+G?7;;3d;=$wGfU{zeNZZECOT7- zG=*%)8P)_f)T6va-YcE)cLp>sa$~|^hn%Z3K0w?zla@l@SP%s2Uv{N^o7lFA#|v^g za97ya^VFUe!`S92w~|-s69VOU20XJms*x)$BSrZkBI-rzFp zSFu*sUHN#?2|EySk1I`C&$3Fj3Ry1!Y??ODI}f!~P%yiKdb(C-kLgR6v*GP-&Y<9) z`d3Al_hKPI6*@LU=5(zL{z`O4^K4M|6ecvFMa;^@Z#tPt7k0nS!2(TQS>tQ&=_ln zL9*V%pY6Q#52Xx_wP;0e{yK3{e(=*GV*5g=tt{DX!QTf>4EB{ z{$-w2A6Ya8J5+LDL-w_^HW=*VD~mpkyvN4R!@s@yvo{ZAz>gF*()@J#(aTVNRd9n- zQCI@v6JZp|An zjM8kjh#;iHGrm(whu|M*z~SNOCP1@P0@37!&h{;?!T|0-5x-02g;B8-rEDJRB@c#U z0|R}{V$dEN8ZAl|m7z;IJ-ftt%4FiQO0tcKcCFx-6FB*BMc!s`^M_1Z8K=L$8IJ$)oA%q#2`MZ`5!^uy2RPK36%gpB zA7P)IqcN)AWNHC|l?#ucAnhç!-#(@nzq>4grvCjr;vOuu*iN$|u^#^669h zV50*m&=3rePp5)0tHC`$G6)Q?JckoTww{X{9@S5rq7q9~u=+D~k52eIFX)^CvmtI? zHoEvK`nbSnJ|PWHp>jgM(}(^>-EZkjYFmCYBWJi#01o@*-;QkFd-}vPads4$M4E#* zI`QWG<0qYX!%QRg;s`nP*|dOMlz7G_mKCD3E_qrLm-6?aU6Qc~Uomu!a>;_msU?yW zcNaeC7VMd4Um#9dU!6Acn{0?(0erPKd_?N^yXI^0Zhhb0*SmFoUy5lxXU$CEOIEb> z-@0}-?FX$5OLhi)Q?J%bdB8r@z(Wnp)qtL_$(I1dm8Wh(GbgEoH3Bl5&pdQ3Eb|@z z28GS}Hle(OrbYAaH@Q|J6M{(*`nEm-eIF@3ZC^@d0p(ut&eT#8))Y+GwMPV!?3TQ z1%ui%Em=6ZI38HWaDI8NVIS||(#ZS0>|lfbgYBc?$)o4q4}w84Ar2jBpg)2aF&-eW z5oHG8V?Jz;H}BRF;OR-)P5^_SQ9Zt^Y4M4HeHq+)=7NJZ>@~sO8nG0G;08-kj&^k_ z%JY94{`BJChUbr8YAK3NMbTK`T0h!cl&Hv1}@rs1P(ck#P<#6x-{J6P-TtnT_1r_#n8qWB$s+Llro0fcth6RR;f1Nuiu30BsaJf`c6;0HYV}$aYSNY`DM`7Qmo4AOkSfaIoHb%eMvjjzKm@+CfQoFp|!LsT2&bX`=G(jv=vC? zBL{QBOoeQH7%N2(fY}>5DN!E=!_N+$53is6IJ|nMeKxd|fpHH5_|wbd;kS>!djE+t z4R&c{f(0B=x;JQysEc3%`$HUUJsq^ws6Q+_(NC)WM9+w}W*!H21vYyOj%D5=8^3J5 z%heA_1{pU$YeDJeH#T*PH({ zoLqkNQWTvj;=L}|?}AzC4r-`g2s|*J;{7%Haik9XwC}XVt?*gC)lB%=-yx=+EZs|A z$mK=5?I~z+ruM@7s7Z|nb!BZt8*i21#;f7S=dl!p<6QkCcGj~^|Eyro{_tGK-V(&P z&=Ig~G@9di+K~48cyg`a*6FbSPD^?i7nW5hc1ChoJmbz~=iJFuB^qQgJK#ct=?nd` z%pi_I+O3RdDFBVf;EE+94C12ts90SL^6(qD07|M+rJw{JXnl`-uy6+xmj$SL)G@aA z2yB#7Q4q?-V31Q$9N1u=hLC50isG3n6j?0gwtXG6v7qT~w4tRjltxJX0Q5nHwFw?t zb!SEbd+UMZMRWEPgiCk&a_>fXiJ;_$5TewK-#pk0yzm>ml^exmkk3alAgchWM!KU+u#K z$5Ir68+s74hFpiG?rf17hPL6s$Y*S#5uD z^GU%H0fHSC=iM!YK)3HynR5_?5d?jaW$ogJ*36J_A(tC|VW^FjZ+iGX>*0GuyU_-+ zg*Nt4wtCpShmAJ6VWVDVM{#-o6xvpMr<}ikuj6Aib3^UunK-pQWeEW*^in6*Z}xhd z0acusntWyhX3v~$Y1(|!Hz^e~E3=`gS;<-_4pDhoZWdYuu!)?tGAFbtGSi9qqbB;q zI>9L*(*@cEaQ3R?UP%}Epj<*uoT);4_?P=w1%6>>$?grreFf{?(wq80&saenYnrfv zPDDpdVG&VPOexZmQAg&clqQ6pU!1VRC#+wk3&r9fvdte^!4>jnB=mgUK}MXMJkQT{Ug`k^dExs6j%uJxnp zH8VCCH+X!Yv54(%>`kMcPT+P}H^2pDkZA)%InYRsKbF2+X=6`jjX2mKB9;)b=_{w( zuz4$#9ybezjI5ieFa30_Su4!oAm}4fYl(4=iW)*O1oAiwiqapz+X8$=t_VM?-tqrh zW6SL;#-K%$`;h`EpY;ne12L8oa52ET(UB{hlydR$gO;Vl*|Gd0?foxw^oj>v{G!gl zRl;wz_K6t;{DSSx6PUFj#hTt>+Zha&`jZCSRf%4~Wv|9!0ChK7#u5l%DGIYTv@9Vv zdi1a5YJ3MaSc*c8L=9NH<%~+J*qSNoYk88HMFVtihFFD?53p=?S!o6~JR^omS0gcF zgHEdfuV-)QVcpYGl*g~M6h(V%9G1;PncWeCUygfy>w$eNMTxycz||!w)V+PCJzec;9fIM;}QclC0o0t_LSQqNAoC)!AzR!$n_phlV5;Jr|kkA46Xyis3{ z03ul3MJ>p8kLg%D#YMm3WEhTs4Fh$G3VU!!XDm_C`Yghp7Phvl;ume+b_+CGP*y@Hl8Zm3yn28@hu=@ zxy=>w7W9qtS#Mt{w$}fKa=ww;4YkdXZE8Siopp^gBYhFnsqG3K3+llBORjI_>pJ84 z{@t{5e`Q6aJ8f;xiA=gVv#0b?E)q*J$Ct2mvXf>mg%9y_Dq5MDhc2!HSwSQq=^oOD z8u-`O0KIbTyV%p0Y~d7?U3m&O@fY4w=9InDF&6v-O#a_K<(VswtTWiP$Qk;74QQ;Q z``H_H6*k|GbDf_^8ZAo7W%)Ch6%iv1|4ofoR~W&{=?$_gFBR5Pt;bGf_SAhTX@Rzw zHlH+On$${ppYy-)Eb4*0{#0mZZ!oB3bJ2s{!{K00r($@(ui>Qz^qaJ3j*gW3TQWVYB;Pc|~E1!zO zNQk93ALQe&AIegclQ%w*M5FWAI8^%2>tBo(Nn3+>-Am)HWlDp+`oOYssr&^89=ubP>>^`a! zIv9Avb)A8Y4?1d8>se|9Mh&Hrxi?0%B1ycy&?zbm%(f+6AG$^ufo{RDfM1_cW)voj zX4rF6wJ-sEiHG2T0vcP)+F;;Jz=R726}j@JjTzVogzs^aOh(E@GzlN<2x}+@GRwgi zG-+Ff(Mo(Qfh=XP*r_gE@YoF8%Y3j`yE7M_$2?g*eipH zDJN1PTUpu#6)we*xcIIwAVn6ruJufvDOhu+O?nAhalEgCRa!IXnK{&(<+r+vPFI}8 zxE`HrV1a?Q=AypGV${9M4N-$%pN)1}rfFriw7}bPE1~5n@EOp^F^6nmsA-BL-+V5= z16Hy36jaG;CPc2Z&pnJ=X}wh|E2LA~6*`hySjo)utdMT5GwvSSa6ug*)Lb<3s+T z2L81*;9kiKwfgSbKK{MUoIV~K7!xGZw^A9JvRWCyvfSqFS0a|3yqlus?Wy}xOZR2@ z8IMOnsjTyHl_SYU#xKqFlKN6BGcbOI&Gu>0&iCBB<3^kH@MJ3zv$!T*^}i=xl|Mqp zT=pMB(g!=#OQQr$!`bC4wf_$eDvyhdC{Fu<%>O~se$w`j5J3b72x_q3qZLWSX9YJ- zwM60K>Rj*FY{7{^G5)$`%LHZ?=;RKD;X4kH5F}B6<8y3K%KLuI+t81Y%^wdJfwF)x zdeEi!hIQWQ20{jaxLigNMEn}@Regy0S*qTKMqst5WSFUY5Sk8Os$;e+m&T$`0r z0aTvilZo7Ru)-0apbH(;FGs#|^y}553swDrXKvuzACqWICZ!n|5vS`pUVXm1Fh=dFBk+ zVYStKbE4z@)HL(%951WV?*^o;7HDAokx|HN+8Mlz(6Xg z*0&ArZb6z1pu(#ps$WtDi%wr0-ZZq#sG)2gbSOGkPp;9!$mM!eN;jn5swTRfsw#pF zN+lm0A9pLIT-Vh3El1(T5|!Z>1%(FxrPtwNx``A*4jWNSJ z0}3{G4rQkVz#CZ2)=aS1V$B2`-YKYHR+4pe6EonAzPOHQs$oXRl;m5MXRGu+_J%R( zC!8s=HFF8x(tK5ZvDy|9U#4!Zzg9^TU0D$e)tTTVbHyc4GAq2w|B_5Ar4inUN@z>j zpq&MmbO2h~DV*|@e#%3+6Veu{@RS5`tA+Bz^-u%fp$63VvioI^Yu+ZLFtFUeD6nBh zn2YVHtQ2RHb+jpOmQlcok)1l^CcycLq82}u-_e6iK~usnFRxSIOI@csaO)^KSsmf# z7IZQub&rLl~x?W;mO1qM_wPe(vSn*ES zc`xNJugC>G_jnIirjP`tp)c7SBOu41avLeb?-+4l%=czJ#XHZ<1r%#9M< zVAjT7IqsEZ9Ns9^OHuw;EJa~izihwO*w&|_e2Aqe%ucyfu*S!{YAhAYL9jJHvXcts zdqg&qxc16>5VaA00?qo07J$<9=BG5dkY({%FNBO(mZCi{KA#NxH=0o+ze3u!HgHvt zLPK?~;~O1QD>@6HMUW$#XuAs?{J{pP)EDK~&wd>CHI5`8g#Ra}pN8K*{WiS$_?wm; z#eReqDj5VxsB?lWv4ugjoBKpuehIsVLvZkCUaN)r$01eFeh2E!m zXbHuUf*TCju0+Su0H)XTtGbyb5`|QukN%|Q^v?q%WKrRy#_OIiL8g~Gjb{b0)PW5| zV}R)8D4McEfP-Jpqv66C%P5VyBF|P$p_H*FE}J$MM<56z1ybs|A@j*2x@qViuNwq+SU>kF4gBJ;i=DSeAngg zDAz;CHjrxZ)Fx-78<}7VkBRzQeAhvbWpaQ(1SI<6!9D^O+X`S@Gpj_;hxgf#Ts;_` zfdbx3Y+!<(I#Q%EX5~nhU|qY4EUdvc^cFw@RqU=Mct^BJ0i$QGZzg|9&$xcSYVC%n$~6tjWlU`=w_ufYn_aBxOG~V_|Vzn+WefnDg7MH1MHy&zI6@cvr)gt z7dq+!_*%7~Mz`cb6eI6^mgbz9gSFzj{GA#)>P-rpsb5Dm;=a9gOCF3mmnb%a zhny;{I#3{#^GYJp3Qu4XWx50pd}1T5N|SjdP5KeB3K?BL6gQ2|shT2Emlr z5Oqgs?61L6lrnQ;hovY_hhLumxhzHDRFr+c+h1w#h>y7xMf+^LJJs0w=G*}d&(4GRJwTZ5pSbCDc3AH;o=H-=!u$9d)R`l;q2zPEcemM zis7mK+)~hnBY5*V70u->+geBx|D>B`LmbP?k`wk6K?g^^vLuHYHUu*WYP>!E&9hh1 zMe3Pg(b1j`W>EWH-oz%OXV+Q+114Bsia$QSD52WIC zVT)&MXhs0G&}hs8(6knBPAMHGw7?Wv8VYv(WvNIE@}fL+P=GgeQZ|N6K(VjDY9Iqh z31mQreJ(5IL35c0iolQp8w7(G{gXbEQYZFSebKL8de$78-DTyNnjC*S$Ai3rN8di77ye_^PEs+n%uzr=mpt zNIroLHut$cxBa%wbr-t1r$pz~giIw1X!IGMHY^{xxCea%Gw7W+m;DkIf<^~AiG;e` zX_lxEFgO{sL`A^|%b>2ZuTZt9)dw3LNr6cC=r__EQH-u@MnQz|IUu9e%Ge5m*_Tur!QS~B(M_@K;QL$a($Qje(pa=eyD-52C`3%5aqfK+F|Z2Aa99% z37$fe_b$4m@6w*BFNyGL{j|VVDgx8s&X9W)M*ddvvVe6|>u?i^_aL2cey8FF22Ds@ zRP|AVU)tgLKYHX1NkG~w-(P`EOqhj01dp#qWY0^fg|vB?qFm*KG~WXj{Ny9o5Hwld zO7i`efCfu2E;XKD5X}3shFlIDJl%gjd^-4~y&I18TQ__83CG|-*MWiu9I3j?kFWZL zlw~ZQy`k|JC$RVjNR2EQ5Jzjse*MF;2yn#UINtx`J;{Re*$G$uKF>S7Mtshb#y%ko z!gDDK`)tG>8>F0!k~22gW8-z6iXz#}Sm>6bT;);}g)<0h@R7~@g;ilUwz*bqm#v99 zG@V@2J2f0!_?+VqAY*{T=xD|uEdUY#ZW-OtuERz2ccGxg_RU9a0Mfyu zBk`2h*4T)BI4r?E9^^-c0mL@7HMIH9KZ-kk`x&H?{8_pJEl3}hD-^K1`E*G=qei@aNpybVg$^U6NatI+iYx8-mOnh$M5D0;Jm&?2`a_Fekau3jeO85_)|fmk0H*;m4W4Q87tdH~S> z_{!ko9uJA@E`S2HEhZfA8^(p->1r?wa04f|JcM6Q-axqoDxGFw7;HXQXO9s&&) zA9YHLrlD<7XFObz?HMBKQ`wF&{VZ%WFN$zr+-ZXl4|&KG&Y zxgEvlXO4DtKH&)FQ@)Z=LYb?YRLa1bEXDDv#Fag9i!2ZrIY%)EONmRVcvjd}087jx z99OtDVO^nJ0W|d(Vgs;&f?B0DlLBUBj;L>ztFCNX5qHx~MP_-Ux?Uujsh_W|gtTnp zQ-Uo&3lo~EyMbpmSt!i;~reqhMSi;JuKI8@yFy_~o>ryvDYQW# z&LYeEZO@y^3RQjQ6;mgxN*U{ccVhnZ&gF`Q}+#A8XY=cz?dOmy(WjV+e2V(GU?T<{uHeH|D zLP0Xn@84T7T0DU+L_w;%hTO$J#vcs!8Qe2CXAsVe4Ne1z4Ma6#L#K!k@OZNKZ1}01 zitW~;rQaCPlaihqQs!R*x^LAuW<)s8H|i!2V(~PsRK~v&;i}$ z4`b)7ZV|hKKnuRyOGV(1#S7z%#Rn}?B8myzGX$;}jSI7tHX8SuvV{q53@BeXPL7>*4v~ z3(bN#&}OU$2y<}j!+8Wb(qPZtP~X{Mpv6)Tx3cm$cB_WMD4*t`85>6mZ0tRK;sH60 zO5j1g#G5(dK@CobnavXGtOm%aKxkaiNWbGM6=F6TPrT%PVV`O!t3RYw? z;=_OLhXjojNaA?i!>t#|K7167gGTgo&7!eQ91ZKhguL@NL*J`V<$BFd6M*g>x&=ZG zwS?VD{e`2{T|PTb;WUrz3IAku8`!`Xf3mH`lrZ>lk`Gs=QM%V1E5CyMUb>?5Y{K#- z7P)6`Abbvupiwt3q!8RmH_!d0 z&{UVcXBl}3v+NcjKo&=ugqZr9+*0DG&DOP#LVb7qW_bPhM-6OQSfcTiP8)fqK`t-YEK|79xP!sLBb|yu zfP)zn{&85p<_}w1uC8$~d)-}Vb;7lHcn9Z`E7G+pLbmY7ED+w?%NsI5g2rZNg%5v7 z8J{||p+H5>*dVYG$Gpa=D7*Dk6qcg=^8DY1=jBus#SPMN3iMpK&V6 zx$7ZjYcOM@EJd+3qyAEq@uddkw}UpWNBJiQqs*8&{v4o?@7~Xlu|-}qP$}YHpyYpn zyKPj-gntw;ur%t@feKDiC>H?^+JImVGdNQ3>1QoN(H_79$hUhcevfx@117t_G?~Vsocq1it3a_P@ zM%&khjfXE^4*N{|*C4%{O#!j49W75eRQ^V%KCran{Jjq31P9t_d5xw4nz}Ue%q;^C zREmYrf%IqA${)DW1%uAAL;$MN0R&JkWt&m}J3t|@Z-KBq&dQ47kNqlv4Fy3m!O@p?E3EaV$k~<9nt?1~-&uC$wE^8$WW?hO6#N3^Z1St1RvK+p$&$FG z90zX%mmT>qT2T#RCGE(YtmV550<+BD7I1|*Wh_w5hzt)$1ZA!bBurU@al`vv%rp4T zq^_`_a|4==q$`BXY@7uRJ$T0EP3o~1a{p3MOOib{)dnm@c7$u1IJsu(BSmeLel7n- zN<>urg1pI6Tx-Ev9pbvHi`?v7)hoIWA$m2KBd(pG!IlCCXh9{M)l^4~je%Zmer8QDd@{|iyMQx`8L zy{5FchfYeJln%FyX~~kR*xaJl=96!ZXdP~jzJ<;6&O=+iHXrZDE#zzR>vZ?HzSQen z#u+o-dbXyt;#DWI6i7@+a&P})#s+&# zJkp^4)A@(tH=UaCWbe6VAnXn-NntR{J{A0m&oUHF7rD`x3DE>KSgJx~iJ1`xhYD_J zkBhBS&5$3?hqEgUfECSfa07ER24FmO=Ko(u-ota^3Y>G(eXFV z*wDBm_t{VYCYPdcD$468KMy}ExbZ5NqUinqT6TZ(sVILP{__4mbqdTU?oT9>5Osp&?!WAYyU?;0V z;336jeurlNl;=k;^?BtylPj3jgPptDSA_rvOI+BmgpEpX_@UkPy4CARU23%6& zUzLlKc2mkCN`*zLRF~BWBm~2~lm#LFgzNC}XNLuf>Z7Dfq*c5D0Xvd2cu-iR3lkAHlvQ&F@V zY*~upI#Rt|YQs>UilQSyuTEKtqK?e$1eOSqI=~SF-`*EBR1Q!X)HMrPPi1(B zj?PZ7ME~Lv->vucC=Zs(4qQ-|Iz`16|NNs*QF*khr9k?y;pp=}sXp~Za>WTHs#^k4 z-V^4=PnKtJghi51-P(XOyiY3%Uno>3Rfs3nKC$d2QLg5~7;Nyy#}6X7ETb;V2A6sh zKIG-sPoaa}7fk(S$SOF9EVf!0I*nOk=w&wyQ^Xx*bk6Bmf-jJ-(~@pMQ~UTtXQ!?! z%uDV_x6XpG;@?5^RzXfcdYEQ+1FP1nywSs(BZt=my}rs%mldEX0`(ROz53m!GjNH?}op; z`Hu)fJpDWz?mU)u2GN?wp;-dF&+8n*7&Ga;+gm1RjJmHG+z0yX=!eez1I_3V?AH%} z(@BLI085W!1o$iE$a9dx180pnR4!JP6v9#`ayInGV7y(5vg6q!+Fv7=qUdu%WS1OhwGm)i`tU>^oqJ51Q8edn7=__0vk9U z_V`*Spy*GNYt%+hG=n8G7% znE2|IS<9lJj}y|6kDug?Dl;}5Xwe1_J_UurvZl|(v7eecrTsMaG~2~XQFJQGftI4| za4L$PrW*wTaw!S}@=FCaVkwF`4o8Br9EAaVjX+gP)j1UT$2lsVJ7OF9Ags9N4%erSAkncZAaMZKlD7S#Bx> z&sT^^s~xt#z`@7U6#de(E))Ue@TSlPx610wXPPnOhn1N$g0im(8{Y2f*j;AMaC9;C zPF-_(qtQn^?5aE26+VkoI@3ZE=1nxV$IJ*wgfhVvK*O;>BuXL|D8s~P5_}^=Y6Rsb z4=NCuaVc-L?vf@E_fCt>d-MH)3{>J*nfX=BqoZGKM~eDJ_N2H>SL|y5Gb(35Dd}I3 zHn!eZ`!{qXtw1DsK3w%#F1ivP*s*&~;fZh4G`FQqeM#5ifX(qF?U$sj{QJlb+Tb2# zkV`3FV$B23Lk--c2F8xJmT8u37PwZ-H$ZOoe0*=yzPJ2uP*EF+z>m#*xr=HZ-$VNn z22LS-^G#vUEkWy7rI_aV*6>>H9k=?QRZq99tEG$gXCRxe(q9|cAkY#^Qg*%X$C37l@Tn-z{ye?F z7C&n&Cmb^yt_iSUgU17**L8tTW;PVvnP*%X=3!;jtLsuTHb|KX;`$_DB>Qh@GsbVF z&gGDY9?@c?~YHH@s=nxLgfN=mz2*#LHNz{#Oa9M#6mMT&A3M|=|QU}_ih5m(_ zus^uPsNlxg+xIqx83{g4N56!-LlkBPl@}jB4Ilsh>+tEPCLS7MVRGXi2 zrS{}rmOpZCLNiitkW1x|PK`}mfxDP6(@z;T(Dy*{Py=661L|^ZuXDZyOl5ivnq|rnqg*MBuwK?(r6o_6;CIQZ_!+wb)L5GRr~$Ql8*mXj2LU$1GZ!fe5}z1> z60FoGk|@pP5e_D>1f2o7#z0pZ=(FJ_KOn>ScQ)J95uz^+Ukxt~v=^j)u_w5IOqQZ> z;>H=9QR?q#>qxslXoiPW^ESS(Wh^hWZv#u$0kV{VJsP$&80U{r3Uc@`NN4cQTee0z z#YK>0nK2@p9BA0~CZ)L)WtV}tW^8cm>k}=7dG+L{;b#RlUTP`Ij`Z^Gf9X?EV#dZ_ z-u=gLa;4)^ecUU{abm`bgDZZ~57o9%e#Y__j+s?nkpfa5?x8elI!1P$0i!zfi6j}= zkiFMhj?c^w&oGBW`eLMlKQZ3K%QE`Hb6)`yIZ9*~^&2%ofJ8afXN)+9;ap2pv{j0I z1xFhpk2>4;F|i^2Mq}L@HW@8ia$My4)>!XGftf3<3b@pE2L!AlxP#3R5OMIPEJ0$% zhELUDdjz$s{g;g;mJ*eSF=m!v7v{jT0`JPort9<%-FD@ z((5|w1XYR37&|ILX8|QuUra$!1F<8p+Qoeeae`-EDGJDH5LJc)M1$-2UF^F-ph

0H7s!CSdA`2`bc@-H&K^3}|Y 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
2^Xp$aa@*}F@l!3 zrWZsrHrD~y?}n~l92pD>+Jqwtr$;&pN2(cuuCMfZj4}B~s8GiUcqq>c%B4Og*}ZTX z<}tjk4B;je(q%_z0dW{-q&!N8-$^rsK69>R7wJymp63XtFPxofd5>eo9pyT_>&nM@ zzi8-Bh7>QlRWJGdI5p5-Snc>Con@S}EYB}c=9{uiv91xpZ1rX7Cut`0H%)r~_tqKU*1dzhO*bv7}kWOhN8Cgo&4;hcwM!(5h5LKEKd8K!%RJ~hN>bK^M8 z6L5=M)8jN_k{OfVdkz%Vkj|=O7Ti3#uVB=$>q;V_lD6Jj3hx9DgmHP)49`iv0W)>w z#HqpL8Pkjdr=4!#X%i}ozc5@edog_ZZ0PO&t@XQ_it<|7Rm8@M91y9hRjt8sXY-!w zlCwb1`v+%F?cm^f(l0k{Z}Ho!#TR7w^SRu?J`{gFd+um=%q^A-&>zkO?1FQ>OpmE3 z?|3T8fp%YE(NP=`Eb{5~HL$7qU^DB__JpO9B;E6Qa|NjVXv21(WV+JAr=kja$*tG5 zDKHC34Q7OAxfKxWD9*x-vH|$tVQJF9!^u&3!{1eF+KxFXlZNx9)^iZ8-h!){Ou>+W zj24OJh(1vTS6b&ntDOc9v7#V2B29cK?9vpHX^+W#mL7es6vX@N?1Na>szLMKanxOf)bT^hn98g3Yqwo7nxiEgT~Ta{}HIj~30ZQ>CzbQOA^M6@b%X zPiwyHslIz!4C=F=Dp#L;y%cS`%x*~fMJY?#_PEHQxb(fZOZm<_l1P1bPi0#;3C9GS zs;oyE6b&*xPMaE_3^Fa&NV@H>k>a0d&^*~^lo>oy2IwYgo2i!(-ZODuOu?YWIc}S$Xn<6-Th-$2m&^KI?lDEBVZV&*C!$he0x%Dr^B+Q%K&H{A8t@d$I&W!!M7Y zR1M4 zu4o4p%?`0MKpuy3rU-CPMNzcI`VrLYLZxO2+%aiqq zU1z`1LMx{JRw<1$1m_OU82*+OuD}$Hqw~Y^;lcZ5Yh4SU>V@>q=B^htU6q}-wIN-| z!2uV`js3(+bp5fu#+hF5o*e1LwM<{OTh+V+^Tct4p8cnEOh;kSPo~L`x8xESPJNmt z2vFa}(*Qn!6@3MVaF#SRII1LiCCXV>V|(qE*VkZbQJ7%TnskWna43P#OYRNX^eN9F zyQo~Tiw&I<9P=DJF16-H8q_w%)gTt_sGYE?^)|vmAVCz`A_t4u&{Num24e)n5L|l4 zG&HXssRw5VnZ$UmGaMtxA?p_agOw>NlIKz%J1;QurI;mo^BggufpB#$51jDa7VvB| zGmy3G#%O6L|4RA2j*B+ zy&w;NwfUsI>jnn4>c3o;3=+>>gCBvE2AEEwfv@=ENu*8YsW!qaKOl-~>R?b$PDKf( z;-k8C5uz*FT^zn84h$k)Fw1Lr3xg-^u`sm)*Oq;a^1G@jnK-}$>=R!#Z$Ql}&4oxA zXFHjk=fY&nA(NaMa&F|VC`?7!W>*wXMY&g2xB96lL~I;<@R1xF?<=zAp`uVU6@{n` zo$Z98pU}N@>3&l?ePhf1A}^6UCk9Q*HddVg>Vc6!3 zSw1h{GnU5tX3ZmcQDuGABOFzDs&S_NEtR?o6J_-w&BGVJH~bFye)=DeCe*0Eh6nnebtTO!Ysg+i7rp=< zwBotgJQwleT|ZsTG-NSu^IJ@*1Cw>0)x%@;q%=eB2_2)D4Vy=0{#_-rG5eA})z&x; zZ7~)b7gKyvwmc76E|}}N_NhtIz9jC6ljZbZUy|5y<~u$uPTXJN6ZctvS34)L^IIi? z+ZM2S4Q3eLL>Z9k^UBGVThsQhEF!wKBF9EGjy>fBInY%-*1e0$PB~gR)bx= zOxPE#T(R2+HN=6L5mMmdh($@46i9$hjvJ zJ8)Xz=-60R)P;h&k!I;a>z>HL5n8PyE*fvWS67Mox;H&AWK5T19unXqaUB66fiXB0 z)qsJxEQMVt42C0`>*U}ol;Pg0@PG`ECQ5phf!pVLnhHx+u3Q;=IwT_osydA=YVAgIn7n({Fi+#eT@=mss z+P71Hhz%M2?23YO#|LycRn%^`UB;pvAgU7_>edBAlm{jvKCgmjjA1~S6b=C4bP`gY z871SW0G}c~{8rBO0Ig~DQ8+f@jFFp;jczK6E?Q4TQ3TDl*4ViH`s<#O;;AT-16|{I zvSWj(C=VZZ4vj8q1CMNVKzZe28d&?tV@4A7HzH?T9PQ#;^13svm;loZ*zlEylau)@ z%(ICy)1S@1&-ST?-=xypqE?QcSg8i7CadTJtnmz(91V{IO@NJd-B-iLsEjM^EnehI zy!^ReELXqyWjQ(I;MmnZ*Y)~KmXP|ol7nOEqYq1Y@J(Gme_WRS;+wi(De6PljGZJ& z(49wG+sQ$qX;24;c#vtb6YvpKKyggg-Ox74#GZKuH^rPePFm{gn1@_owX9nX^V5^< zYOy;(d*D3u9xK{7SM(W-1~fBp+Ij}~Z9%J#;KTR4Bd`Dd?l=#-@5aEIBghOkv*eit zpTXuiz(2a0ZYm^wprBkm5Zx~8dVu1s9S3i~&h&fF!(2Qz(rbOs-L#&U_#c-B7+dW% z(T=q?{c+Vl!gp4+t(Ywt@P>wLwdGoHPvQG{C1~h+gxRUM=fpT(lyn?#96GD!aqNkP zH^g~ww93Ybg*_~2b4lN@bh5>nENk;UUZ3j91&SA?Kt{?d;_Q+@i1JxjJ$$y7T0XVX zl&2@JKTs`Tw$!ON>wRWBhP<-otU9J8cTgvWZGb*Mfj7o6Fp^R^Uld6fXB)?YV-wim z*8Lm9I9wZ%P6L^#6s{MOOV5!Q#+VmZ80y$Tu;kG7g>%Bsj>?8k`g+Ophpo6$mos2D z4%-9RKl+^M1>F9Tc16(|8$W#fJ@1ObR;Db5**P}!`{HRRvH|%secsEcA6YoML}o)L zA+OT<$H~Aq5g^F6tcfYHrR`PuVE?^tVbJ@#T4Q8Wwkk^)*a3h7n8a}5ZE(+IJDwb$ zmQ#JsWwVQ=qg{5_?%26;B70Dlruj4Zgirq24m3>5Ip{onq=G3y#&m4tIbgkGBl=Gp z_LfaKDRv~o=IV}So5;>a->QY*XChP%wSx-V@NTlLuhvu1T#6VIvQe3a6uq}p4z>OX zlXk+PB`K;%=suhq38)6FRjauv7}uEpn(AUh7~>b}B{@4h&4Z~6GV+JLB_;SDrd1)8 ze`Zm|b*i%TAkI4AnS7^k#9f{1^))oeS6GA)176W5Hl%qI>`-?_si+M!i>VIV540NY zg9l|@hSf?t6-84WVk(MuMNt}$7!DpA&O`#_NBW~((reX$64DoWL-K%H=lipj4mcWWJ0Y_}7p70vPQ! zEFBv<=NcRImHLfcYj$M-vv??LZ7@{`I&_~?QT8+y)M$6k;0r|JSQm%gKSIX7r<=lAXRb87JeDwRcf^~ORc-m{4vU2BcxqRc3 zW$E=#mgTSiUMYVpC&%lzMay+Gp|eTBs!Z)jcC#8;8S-a`x=x?q#LX)eMquiO{D4)p zxsj(H`xIDlOe(9vA;~d`642py;trv6a!oyRuof!Hx(9L&GCdntJ5n|R>WOZWT7=O8 zIN7GGS|q8l$)ZGQ&sjs_#yOx_27Z>ew8JzQJw>0IO*;#xg;U35-B~^fAIkIBFXzXi zt36$!-_^DM)8#WezgfPt^`-LK+Ut5dq$zH?j~rY)Ef3E>EFYY`U*0==ul&WCBEHVQ zujiM)^rfA;iPmfRvSme%Y0WPbH6l z=$k|~BMX~zPuZuM;yQH4%fQR-?lJmQ)?MGvHf#gO-+aNujC$HW1=(b-&-W?t@|X?n zOO&b^PW?zD)ng^c{v2=814G|D-*e#5&{1dnk@8M{7Jic~TCvYYJPn^3+D;2QQZWr}LVg zJ2zxE0q1egkDrqF?DSj_8&B-mz`3y}J}mHwA)X%$@z@;&8xZ2?s^AeV1O}9rcHf2x z5d9HTspFSzO8@ILQ#`DPPl@in7^VyQ_#BIW}s<2701j*cSFsyP}+^UlEqbd(UDJ0=wZb z)dnZZx|}0dir~P8JiO3$z09|vaf~mEl+F!-yQ3Z`w2EvLL)M5aZrx+;e4byyGKs%DbHuW?8^bNck#sK2bX*0iSh@R2Wo2ljnD(2epDnNjpzQt z!*e{;MVh<7k69s-=&#o?OJB3#ed}J>5KKnNn*7}AE5aO{wx~^K6>*+jGQ=Cp{kUky zWC1oVPiShqtmiZ=CpXz8J?_m?L(Vg3uc@r((WhEYT96SXV6exhE&U6FEUnE)NCM1jTBX z;V29(8-7lup|n)ZQv0Zn zf7%;R;GqlJ?bOhyl8q%QM}WWu2S=b?)`doi%s4tbEKiS~IJ#l0b#AC`)(&B7*i%Jr ztXyddh#U#5JAzXKJ>zKDl(XP+RWAtrtX=fGXG>I?MPeikd)4-z^ynoM5J&B358BoL zh4X>_S*}hg_odP}SGF`21*Zzp7&tcQw^)c(3#OiH$C~q}>LaH5M8pbCi>vw)9+`;X zpudR7WQt0@AV0o1^b{3~jp+Jd0V_dHN7Zwr)e&m+k{kpVT4WUGzzPPl40caXk>SZY z9DaL_(cZQ<`Bb*idFI-EB}zvoT+sV#w<|Gk!y8b?Xpn%3xtN)U5^wBf)MG3YtznsE zL}N6|6JQ%?V@r#(Xu8Att`v^LLkH7N&K2r@pv6Pk73EZm8QPJd0q3*$c4uH!hnz{e zv(*XgR)T{=k5}TVj*_mrW`LU!9O(K&&LfpyF}DiG41*5rhCi}(M^%>lL+fQMabdD> zs_HWxZWHmaW8+j8we~o6pNgkFho}lCjUN1f;K*QY5;_5%Y#mGWJi-wLlMW6?eYoyu zD>(X(!-*~@IE5NB-~%>Ye<>KQx`tCxbYT#!q5fjA(4G7DG!>=a6$P}!n; zA~ufdRFrNf6kXg*L&>QqHkP?~=^XTm?)B%+QrZ_VWqc}sWx%LMr2MU~XkqwYjnE;g zBzYuhgV3S1Wuy2lLX#daXftiP!MCKd%4p#YjvFD*;fwycmwfV#a`nr9vt0hX92=i_ zBl?Byny)+17tTUHI72jz<&sD&jjd&kgR-x;xHtv5#%CS1EpciZrliM+y76-Vs(kUS zPnMtk**D9U?h&iHZ!o2aG5g;8AC`af`+r_O(t00+`>`+jxHcIn4gWUXf@jHj6Zo_6 zdls5`GR@*QsfKwx=9a-I4^@`+JrDoNBUy2dS`w1=_2*WK9vb4zDr@l;O3!K2SRDzQ zE%idRUV|j=A8!TM)ZZ7^yS6%Ot9)_iE9EP<|4#Yx?yr~eeiEfMp24=?xa&>*to$=RdwNYNd9-Z}Yh`R>uT%8{nGvQj+{VS3-C z2P-{Li3bl~@e9ItYQC54W$K0+7^rZNS)5Uh%bSvepR|kC#vsUkPui?kc|YdN<8>{< zJmMd3SvIp*S`V3&%+G(5z&ZKkl;=Pl=8`<1(`Ac5l1I~v2^yT|$=}!0rv}6tAG1mZ zc-{D^*qLvU5;n?k2)w@5`52)Scn04g_g1K3I=PPGrxODb#(tNtbtunsl?`uP7&tZT z%#aNr;~ZmlUB+>|=|{5oBgGs4eeHshkp=L!;jEQF&vcL9JAGXC&K}FLEjvfm`Gu-% z4&T%DJF%|F^*h`5%C4q&u=@y)mqc}+pVg@-Pu})al!H?(VtH|_D1n&X7>*6u7&sxS zjb{6gHi`!lT9w^DLKE`Il}mK0?n`x5&GnTRHFyIiYk zMxVCaB!|*x$gP*@yK-i1F2}~II2}k|twuexhqbC`Lt2oolX@i-Qd(w!-4XrZo^v0D z615V})QGc#9a6w&jBGA# z3oJJn4i5eH6KMmS7uWDZL;SB=Xn2sQhh)@x5Xuw1Ao#KjZH(b+;5jc?*l~QE* z)Lcym0!X~^QhDdt&_$6`QMmEwLx1ePrl~0D*iarP2z`NL<3QWJ;@CLZV_g!)sQN`W zq$^KJQQG~J{iHJjGOl&R1rU8;`BTSkPtzcDW&-I6oWwJi`c=^*H^>O&n#PL^6}~G7 zl+|6sbjR3#`)RrQ&om9?D~j0o1vxhEKM1|H zy2bf{GKk_h)!3C|q4@gLSwAGcJ^;KxL%*m88v0Dmk*lj;a`!I(-q%X`ga1!cRJ0(e z#>?nITOjqRW-6k^fam_>@(+IN%jF;c!@pY|+`p}@wbtZ8X9efw@`r!?XXV@9`>?$C z_&|7)@vg2rZk$scs7eOM7!C-t-~-~CnSnb4Jk4QJrfG~R$igHGBTiLez{2E0Y;oK4 zIA=lUBc=K{Uw0&@!0jpGe%@2}B|U616}-TVdky)xPfudL*nGADt|@22GC!zM&jUO_ zT%BmWoAORA`gB>^xv%RqDF+lETgKXwQ=a!Tzj^l`m*2SiUz9KH{%YA>xue?w0k*?~ zYdR%_c9pkEx~HtknZL7g%MV3Mn_9F@4^khTzFWRt{#E(?qdyTJ-LEd*)jFh~Q26i9 zl(#lM?+3GY&fhNIJ^w%fXwt*wd-2ISmb6~bl6HCGAx!dhzlTGSUgn-t10Dk{k10S~ zcuhKKIWKawgDGhny7&zVz%)xilD?rCBq^UJ9H9I-GB+8^*#tLZW?TA#K6R6pWi5-* zBz9j{pPJP7R9hkDXhUeUdjTpo~Bn_D?mzog2p%cHX61J<$H4?c2=ZLOJCkKm| z-jZWwTT@c_?PU-^$BrjNPU6@gs*)E8uE;uxwy4#D9SaX%MCg(!DmcIBPj*q^0Gnt` zt*+%f7KC}FLaCHSqDU(SC9AQHQm|2u;j4cdg<;(p50#KY6ct?s`@w;tz90{xu5~cJleI5wZhIDTbhotu_Hwh*&#&U{GS~iYbwek8P6Ztv2mfd z$+X{VY}AN=aCYRNmX4$+9&F*^omp5nfG7x@4K!Noex$58G2rJbi87^eXt2u*oe{i( z%omMNf;Ktm?0PhK66|9XjGf1K*J@ z$OWFxcEB+BvyY)w83(8Cq1+j@*=71dUh_de;!9pPnQ)Tnf=I^(5kV^Vt|%-X`i3Gl z?%(%fuaMIREFQZ5ks>xU6@}evfVI1#kdLUExDlX3>(Ljm(r&+mrQ!V327e_ifT?>y zDTveL_W(S|Ok$4Y7r{4krP1MwqE`AS7R#57v$K;H8%8P;)71+kVCQT zJuXX+AC)CVj(HJaE%JMH>yD^cf79e~E)RQUNPKB-<}tZ~`t*7nW6ltV=IvWrUc zHFEu;s=4|-EU&$KuYC4reyY6oK=n4qqsLFRkqb9fF*)H{o^xDV&m7vHgrd1RT2R16 zcyu^jVg~bh<@3sBuxr3{)lHA2dA^$aS|^7~@&}_$Ck6G6!LvNii{K>g*!k*M>r-v6 zmD|7Z_sZt$Z6A89(viD~u~1@hDnY9ER22XcDsU&!e}w5A;#dJcGbyf6(!-;T3$ zUu&>0GTvK0i{QJ)mS{`_7|q04u$BeT7C;%3&Z0dF?M-RGUzBE1yy*trjBj!)9YFCu+YgZIaMN#BK zMj2$ph8-KSDRNB+r69FOI&KfPLqyR2QDVCTLrOQYba3E8!M+T~22Pq- z1T<_Zw5LCfCEM&ed)*5gbHJkfM2>`5{8Zf5E)=z~?cXwV!TJ}-Pu=k1cdb2;o*hWN z(5f-BlzQbx!7=5(aBQS=1E&=A?5^JSR1{t&;82N$Sec6Qv^>>sZLFJt4G`K0?lCTK zZXg#99qo$S9gGDWU)1kKMrpr3;rP;W6N$+Wg5dCu!&a&qY6YQi4B9B9@$RhbucR@& z%Tz}FDcOVJsSPd)CY5!_(33Q&sMFYybSIdAM#0nHB%+bDCk$vkSlFOe6Ie_}7t8wQ zM%li-8xb2as3CPIlIGGM$gy$w@S&!nXuhomx}!8S=)+*vSxz1Xne`#TJ-Dqn4I-wZ z;MgE)Ll>zJgj44eiti)}@J=LgVWwU^sH^(uVh-^8!_II>lth^I_tZ-r294@W=8k2*mC^$B9DoQ#wT&MVvfA{UO z{HOn+EdAjhlqIH_=$g9v^ry?^TfbDUKKpZ8%v0-gw1*;h9NFOfs$9)l>OuJ*|5xP=YU3kf%%7-Smqsj7___l3!_0gsuP+3lue z7SE(=ac1F8Ry~F{27}J`Pdy|qYumQFZ~aQSt%$3&+qcVyTmM4NjX#N7sPxbM{6}(J zJ}$3qe7XGW&X>#Qcit+mt$#vuZrdxc*NkQNwaM=QIF#{Co`)tL+Ahm`r*D^U?EhZ* zKOX(l@|~kU)9>b?-fF#4eqraU<;%B!tGu=QYmV~Zk;~iv$r(b}^Re`Ds5LrRuVe4x zQF;ILopS%^+vR&F-z@K+zo%{O&gery3utl z?HpNqj^lOT1lseONQuc{T((S+ZH{~`>IBy=Yn(*1eb*I9M_JHcjg(?o2c#_@rZdMm z(r}AE0AWjJX_inx83s7!alZM-JYMwzp*!073dhE_BJ7xM!uFtEI8*f;X<8xoV(#S_ zww|J*D1dNiNM>1YOxU+p%Ygd$~ zCvtLYItxyLbZ+20u`>n7hJHO7(|FKw41QWk1N*isYhA`KP^{A!zePj|lBy*j`T#;Z z_^rY*M4I*B?98}O9crI#Hqh!{I*^=ibj!LHL}swF%$3$z+>%bAB?^M=bXj|mKRbwI zqed?H>J7Nq;%M*_wq?s2tzY*a>!QR5#{-UyTbhc(8XIie%R%%r&L`H_*gIDcfgDw| z-BFTVjbLijskt#phP>V*DuP3PEWx8+V}ViHuRdiL70I_Go#5cm(>ki710uyax`R}f zhRQ^lsByN|1F!uCjl;!!&A>BK{Yn~V!8eb}C~@jJPLv@RCw*7h+~F=#nGN)Mt~f+! zZ0&BB?K`*1n%+D`#D;jN&T~07j`k1Ai9Tn?ih3aW0iz5SH5O^1(8kNlKH^hsAcXN-NPfIohK(Yx)j{UTB4r2Hj~dWZI^04J;P(6pnO) zzJ0*BlQ%%_6D6Zhq91_84Mu}dVZp40!a}O7fx?uURjr}0^Xe-Ru_4C>dGKi0sdlQ_ z|L~zBHcs}mD~gQHaB8T3h}K94lop_|9_-l2bo7;P5FxN{7O10ULmjjul|*5F63HO7 zp;Mj>x8_^&GhyaRoU=9uN*P)6$?z9D#2?X{MrlX?!XQ zPQe2?GJg2|viyxdEX#lV^-_NDu3mo1Ie{bW_@JyDYHlUF2z~yGrLbcPQ&n(ma8Q@< zigr(7)O$+Z@Bgl*sQif>DRqiUH(rBe>YFxZJDb80P*XZDS@qMY2~J%m2uDEkIEH`| zJmwQjl-{7iscNPGL*BEByDDcPz-C_Au$n=xBkL*|6c01taCAhGo%+76d`OTrCwEGd zLkk;HMJ{M(-&5w%2W{}7zInJ~btf!k&HRuLrF`Vn*1dT14edhm=I6@#y?bTvFTSA? zMK@e2ifNKbGJD}?Rdn@U`RvA**>6=!Q(uNH4sVPnT0p=jnwuXD&0*9EM>DdmWj0h{%zT!M~O!J$D zjL{8XCYeV!E_YASr-Cn9nS=qEhJkNr{}n9ez!PDbU(df!B{t_Fw7;wc&yEC*!Cg_4 zBF_|egfu=}XR$PA7*Zqj)OA?bZ8QJTlf+;M&uZpq$Qn;favCt>WM~ zV+Q|i6o3-9;~&a{a-vt2rip6N(Lmhk)Q{?vA{scRe@II%V^wMHIHT%(u-3!Od6 z$tOr@8J-LfaA+7x@fQj310dkd4_MXOg4p#}J}Stv^wzjOfc`|Oe{kL7bi6*?{E(Rs zH$(lJt!-_Ut$W&n=H8vMzN3&F-oy&mi-*cUI+pQ#yr&%yw5PF~LtCp;BaCIKS)J_% z91!&B2cGIfKdR5?D!%z-11xaj@P;c2;Ng{hQG~S{L$f9;C$NRDws;Ckb!-qRVlpV@ zQN%HERi~l|V0Q6ajJ%b7$FWZ^q|Bc-F-1j(ohj(ji=wJt&y}K`8qnx%Np*Sfi?Q-? zgfx-UH}pP1uY~%AAEsf%MmRUtHBE?ZURh&fTZ@R&Ul6lmF;j(@CX~CP^ixrAYUpND z*V%}EQvcCUT*N-3huT+k2><>>x@Kr`6wnYQher>HE1;J4;3=B6sOL2f^$*B&r9IgT zPR)>_sinbak`2NFNf9y?Wm{85eoMQe{Ib^AVDZq&sVIzV#;P0}OF#TxS^A^jkF8z* zsrAH_}&Ml{P|y$C9T1+gu_u}=H>M{l%`WT+9@i!(RuOUKmU#vA640` zDJmwcU$B99b5za^Z=wv8=jXl=B3AWaE2MFNrz{ zkE+60UbNatyh)ziD#vaIDFgILGXr(9iWiW_K-W)@BI#CHd|T7RmWirkiio!0>&A2| z4h|lCk;jUr(_Uytw^H=7?j zlQ#OVzeG;=Ps`8z>}%z7Z~k)m#3x>nzx%BG!5@CR{M8R0YR5Z89Pt}c51zM9wNBE{ ze@V`33Tt=skcU;kG5;SZk*OLN)zotQd?ZAU9D7NsfI6zOvSvR3Nr4{}-3 zLqD$6bv3o+r1@ChwqPKfpEd`Ov5a{!29M#!8)Y5^megl{6dxm*f-S764snKTwJkH7 zcdU&5onmCWcXGlCG9pr|5;<6sX2I$U2`LM2*rcF;!doTclIyiuxx%P~K z?5#?d@A^K+cSIDJ@A~ zC+f2$$>#Rq%+NyLMD_3+xuQizFSYe;b#9o82;;18H6R%I$Beh?^%Gj7Z4$$VZFGkq zkoo9RnSgJm`-E8~n6p)7A%YKXnhxefg{Kf?tDL4Lt%i2f9VD|O0(4&0G?Yz+(e6BW zwQT62EFBxn(>j;2bSy7EpEGSy>8)e6YHnGU;-$#WP^Qi9RN728w9QgZoyTDGVyu#V z__&KnTqzhJ`q-c|7PK(qO9z9eX=A4n+9oH>5{?Z`MZu9`irN=;Z0N0a?uuewuBY;) zuI!6?QD!y0dL!j2Dsq%Kl#6;CXg=0R>Ef)1 zg6an3=^&b3boJGVT~XSpD2xp~td}3ivGK=pZ2ZfAsk{uPPredkF1g~2)SUmNKmAr& z)|wo?G5^As6{+#5icw`Jl7onjPkmaqOC16)X;%|ZZ{ZE9#@)*(vjF`(tB&At z<2r9hFeO`0@B0BF^dKbak15)|Xf!nwe^v#fTwX9R>z)uU!L_!?P5`f!WHvsvJ=ZZI z?j$d2&N}iuiL+>aK79%jXGh8DH!Jyw)HQIzQiA8I`!JurX$_x^+GtZM@01)a#xc%}_J zhVWwJ({E~Cx?TwF?H!bNKhUlNhb+#gvKLZUjy!XGHfgQzC*{qb`K9u=f8%eJpZ~(= zG>z~?GlRdTN2EV0-}=U%NnR~9EAEcq;JH>2lLIA9yU3@2o{F+{zdYEjQ&F&=#OF*ebN7$;%6m`Q73I6-KwHb^t|&~;zyZM;5uVB+ z{MK2{04FjcZVIYPY2@b*xmnK%=k~fB@au~5v!SmN>OI%*&xxirGKJz;YdaBFR zVNW>%MYz=A?h*}I=alrz=O_s*qlOOQf|EV^D%={xKp+-{`yEsexS4 zR#Ncxp*m@Yy55k0E0W)fj0$)r$a+kzp~dcR95w#s955o&D+q%W<8zrUMv3JkI-wfo ze#pUdCI&P~Cq0HAif$ZfLwILWQ?j-nGV4 zQR=QJeyCPI;na{3>!=MoHt08f)b@zjkggGwFZHJ+b&`uY4xMY-p3p$9Wr-rH7M`RO zx9Pr2iL*jL0SIxpOxi*|L;rDVx3yD- z+VNjFp}y?+*9sA#;Z1hzn1r_yOqPw&Uk6KmFM`)~buC_wvV^U9*t}udUfxwdtoWfY z$APa7qs5hyaM}^kr=>hPdtcK~{;2%v{=X}~zxS`o;n{b@_nq?A?f+2=l>YtlbK74o z57yss|MF<)H)*0D@K(pg9|f7}&&bJwYOiP!(y#3PhPE`kD1Wg3PxQO{*=F2`c5v`K zrPclwS^De0_T}=6zxai6por2BKYF4_jYGd2SkXQ3SkEix`hDEiE^iSzqmvvXeooOt zz72GTs_7NCo_BaAvffYvy1~HL@3wBd`Y@e?_gq9u^5R5kPwmtj#j}<2-~V_2xp$2E z+V6h7{ICDpKUH;d(C8ugisz7XMRec{fL>eC^D@Nanfw1aym>B=40?G1Pt7S*o;LFQ zLY){S5=f39*DK9jF%3}<{CfP>M`h3x8MMKh&pH})il&%a5CzkHWUR<@UViOY|3>*o|K)F6hkyR(-!A{x|NXB8M>Za~ zg+|n&9-u=!9Z8>_&aB)8{BXouhx7&Qf`$(gN_IU9t}1c_z2tqGwVc?YF7Swy z<(X9crBirE+XPbLf*p$V+!JzeMh?Od!9qvUqsG~)^uxW6@yupwfd3(U2$Bff6sZb;5s?e zzj~vpIx#NMKXRR-GZ}RpOL|e}soBFDz$bMrw z1?76CXY$4JWgWKzCa{rhp=UkgYW(w;4h_FdRep0#Yent6qNymad&^g%C^))ZQJ(&= zytn_3cSSkZ*ks#OJ2hml*|C8P*cSZhdf?9nluo+hEGs5Hj*U1gVozh6-Mffhz_CGe zAF^`TS)<aJEYOvwG;-fq(iD}5;9$zfrqYhm5R<*LCwdtWP6}?LdAJ7Ax!s~+=P^%3 z(Ha}8Ue_QnS(}QrHHeh-;-R|dP(Od1qXi*bh~HGg{Y(xHG{-og9ZNV_^oP^s=u*?w zFJ$NI9)rw`1%8Lz!xoUcF8mlIP7A7V?`0XDE1M$F4GP1Q!#7Uv_(4hXZODLL(~tk=U4@9gqLp?Pxv|2$ ze$ikYb7MbNB*38-rsP3^g-blmgSO}}AK@?YmRTQn0!9ARM%5Drw2-6^MvML#Sdt-r z?4S{Urj77}#+n%N7J?hiOD4k8kxi!k?i)RrEVC=hhWdfC7baC37}Tr_!ZZ{d8#ozi zf55R{v~|coD?xSL(PUhw%S2}fhxq2W&=0l82M&s=^GlW42}0X-BwMrOOCR54H2Cb$ zz_HV^F73HJY6@}?17xabt&k-C0$A|oeV^91J;%}J%G13HBF-{j!)O&L$(M`W&hXagnqkSQGU5x zYAVXr*sdsp2E`cB6HU3&BEL)T=z4ZPp&%5?QI*AsVa8Mxt)0UzDjf=Fd1OE56cuj3 z+?9E7WQq#Ua%(^AbtCSZ+@z^1Xim*lT=Ico1ev^#C zsY?&&;H)V$Q0oP0;-p{=4<2B5>jO5|A-l+6sMz>PJakfd@$(O=YHS>ey3$Z8YL($NDF(CZN#xc`Vhy0`2+bfW`4iOk~xQv$_bZwZz* zq<;}7y%0N0(v$Ej8%6NPFiQSl=E0*4zaLCDkZ~&h=qUN56p8(x%$e(&#;<)|&JFE! zMiim`;tfz-Z~2W*G7S#D-^`(YeD-1a_TitDfARRAm9HQC8+GwF%iZr=r+O%h@f4h>J9*|gAU)hE$;sae4~%laux7^xC*RX})E2Rp%a7|$ z55HoJt?It^`X?SZV&WtH_PzVgJBluNUv{-ZZ#9-rWNbcs_(Y$FBG=8##xG;(PI>e5 znyMoQ#iJ)j<-PB}tKr4V2d*<+ySfa{x%O>^dlU;XOeD2GR9cRAqdx|i5^)+qlDA|7Ht8bOR z`p$P;%5}qYYdAJ8%Kz{`{c8Er7r#*6c;i)QlSY$z^%_yITK4&uC0_ zURmQB7I*0}^nXuLF`v;kU^g!X*aw_O~{7lh2+@sF5t5Mun?v>Ac_D;Ebd$YX#?q2!u-A9^YBpvI% zwITj`fGhVNJdl%Mr963v<3jY>#`yCZKOcN_TpoS+ME)1x9Ew8vxt}fv8awa)P>T}E z7(3A-H(UBWK&>Z7)JdnTmQVfEn;N%7S)7$`sqfD9Cj49uk+ltt3yteL>ZkSf`zpUG zdr!3Jw;VvP>G|*e-L3N7@9vi)t$Pqo4P7I8GykbiJ}6)M($7n-pH-C2o$^Qv{{PPJ ze6H;7Z0I_mUiAfvUFev=BTF}5Lq9<^QPZr_8Sn}4M0SFAW~Z^HA>+8Ccmp7J#{}eg z_?s2b9&1<=rTK}H0hc~wyxZhV;+t+oi9f+fXtx;nt$~)zqr{6D6jwW|Sk&RIu5P7fX z*(2-;HAP9c1zS&l;nW~{L3L`}GA)DXLe{b99UIW=Uq-U+H_1UXhhjX+imrPcI*yD` zyRb;H;k;x4jpM`biX9$QPPy5IFo5)jtj2|2DGs2N!ILMKaBc`w{5+jUx1@C1um~v5 zA3j@!$QkKXe+TDl{{5tXkRy(;?QmqU-UaQ&!NC*}>SMdwaBMs+Pwd#(^Y*W{rNxK8 zNGG@C8|||EL~hV@q0kCP@Q98mk_%0hA*zyXWG%Dof>oSI$j@N$p;I_7NJuO_gcGf? z^8b_f-tl=}*PZ7_lk%d#CMwo^V6Ps%5M?CdAWWOg#i z&cvJDWOg%?jFU~ANp)g7mSoxLk||LlMTuhXy^{bz0xakq`~98!-1mK65EL25*-0`N zc;4qex18I{Irp4%%V-$51)OU`;Pnr3ImF~2SW@K>sxtu6ud)!_VjYB3^c%7mDnA<| zZF5B_AsfM2(PBP6c0ey=(^bI zSH4P7rFR3}KH}_%>;RNajFf2RacxdMh(JJ`)fjk_jZijO)Y$&d4~vmbMBEcXg!a_R zZ$j7*HC_4w=)o$9Ao;HsE3py*Hy*u2Ku{h;*wC#gLLlg98tHj-9KjpvCI3SB6T*hW zjuju_E0s(g${JFabH*JvcXls+A|eLSB5-ivMuEchu;fA9By27fN$ufzQ27PYf8`~Z ziW3PN;Vm%~7e$#cE)hk6y*ML^LKP;rqO_xDp_^MToKr&t4JM>ur|cp~QJ1k@RBo?# z${x5e!XQ+&`oN8KqIXi)K3OFn(c3*!VZ105DC?gTZsbda_QXbdjPjkNpUKE5L9(@& z2|Q}3;5Z`8h|hGy=oQ;YeL2=gI@~qw5r7965m{{@qTZx>*RkP*4K~p1pxC%+Q#Zn> zQt9JXQHhe!g_f_e(a~Xf?QD8zZ-Q!Q zzjyr7l|)1ZSWbY9u+0U4NUChtD+KODk5;|_x&Ioot@om2zl7(g+i?3x?_q=22QsdzSGre~#^GlRl&@>4 zt8}W#Vj6Pd<3s3{#zV|WUKyK&$`TG;m8K9FkV76M}KqgRPhO*hc$|m?Ch!jZFu%rUmXi(QFEqN5@ z)&TuXo8BJke?Ur`yWf|O1jOFQ0Q%|4FP4K->Qzfc?PK?#g8W1(=g>XTDmdL z5A*&}!~0?KaCIE!5!$(~aI$Y8B`2(sM-fETlr~Hk zMASGfD=og=rj^aJJH|d}v&wI=G3d)sT-Ue{yZ}eTHQHrMzLgdq?ibyB00bAYOKZKB zvQcUL*R`^FAh61%cQSrQGa_ti1Ad9QYQx6u_S$Q2*-J0{2|*WATw2B(hlC4&Q^cVS zd!H#&rrO*&({1Y1N#38NN8-?-lXm({qcxuu5riofqF_G#Z9E4)3+B(VDN`pS0yD-r zL2h1f!47hL_K8!?HUP1swr+ymzv@=tH9&wUw}ys^aFf7+#~j_;yVpu9@@@XSxmI3L zW}VF0o44$>HaJWqHY+ifyY56J2ZF%?h{^j7p0a5TV{9(rh2VCvzPGkswwrI7<$2$< zc{iu>jaFJ!K{@K}mYZi=Z7qm9<7D1kh*3vQ+Nm>5b_Ifsh_jJ^t9FP+b73Duly1aW zMckM=xz46hKC>0TLFAY+d1J9*U%BE|n?7xFn9EpyB<7>}Ef}sABp@9e-|&c+f2Sim{PwvcI*rY`)NCV zq)B)`=URhSR#j#-)%A$a%(RBdlWfeGazuW*tZ{y`>rgq{#7%wV$GJl&>!c~wHg(Em zL^Ecu(GvUL)giBT32|^w zd8Kn5VFvG1py+Yc`|2+_LLsXsMiEk^F;;wZf;=?g~jO(x* zO7KyJlB_voeZ+6g6``e7k|d@mBeL_7g3h>xQrT9XhoV*fRFC42#D=}dZ#YpMIfpWF z)gD@drT40mb*PlEx)nuvjAiNY>s}>_f=U7AOqD6VW4(Gt@KJT<^eL4|743c@G>f*w z5FELS!qZQldf!ba%SQS3p8v`!Vu zCrsexjat_x9yvDDu_fxz1NFE2@+Ir#x>s=nIL8GGqZM$%kP-Xvs)M+|CLRKhBPS}e z5U`k$OnFraiJ=n@>vU8F*XBc;ZrGfLp|0nN)57C~y7ChG55bdl#^hPXt!|2#n%~@LB+ajt$DhPpmO5V18xFM@f9}UI$Q&h;O;HQCs)RAz4&1(u z;9ok&?t5acEu25q>T1W^*s+yX0-`PX&rVJ`1^+n6$a~wkdUEp~;ESagqV#y!yj3g` zh-ruzq78E$Cz}oRwSLN}rW8`p2|}RF`I!S}aS5k*!+AH^nkVPj{5cK2fo*EKV0-uO zxBc7CS;PD~kb_CE&m;3nI|?zl1P9#C{U_o1r%s&Bc#7To~2gSO%AO@yUQ$#Z^HCHB#cn$J%p`^4&ggz!sk;i`qUdigvy(;#SUj79vB zR((elXqTP4kJ>BW-9h*RJgip*)0j(>IW*~dF*VIqA`TCAGk@8Bo=Gfex7g!PEwo#1 znn8WlSS1La2poM7O)eoib?ii=ZQ6RoHow1{@LS1~5UH_NfJl|kOMFEmWX$I*)=jg+ zge5Oi7TL28F9gY)Vf7QMtrX&zIC};l{#?1zVJE1gZ99(G+BbF*XA^OB$_x>h6H6^Y zYP-=)Z0~FNj~3^jkDCgfZ}mkSSa3R~4W-7SHc~Ra;o)0#AiFkbI>GDWWTz3up}RXi zvb~6+H23T!fI_v{jG{Yje%T_MQaanl71sEsR@*Q6XZ}2_V1_?l{A-gHYFl|od^}~h zCvM84Hil)0UQCB5b9&&Q$0gt=8m+$?TOy90Ijb=xLthAm!_IgaV*a!7HevD%`%l05 zQy_!$Gi$E6DSGK|ufOr0{nmf{8}_L|)X z6OE_C`*zwhU-+zj{&P>-YDKmv*p4id-JXL>_7baH<(|je~4+%JpDBF_yF_Pbmqiz7dH~|sDoSj zKHRj^-hKB2`~FMcg{WCaek$zqpMS)j`OHJi+qIN0-!4OBIez>U*%`70H_fp;xF>pd^ohWB+X=P9K@+}_e zBckct1>g<)JgT~vB3Th}UVddgM2mVjC+0e#q@n`Ok@GDO+t!oMjrQKU_u=A$!=`eY zed$XN+ozwrpLQ#y-?GF(nChnQ9y)Z?e*X`iw=J9BgFsPdOP0*HFaE@1D1NQ69Xs~> zTLihPs&Xgj9)iI3$AA0++!^oK;>9!V*=HZ{{-F7H+O&G+`8!w#E9}qz{JVDK=moe@ z2GGaB0Y_@WQ%j~k`5MI}0W{98A9$4bM7mx&SLcBk0b8SgTC3s}#!2_sxY^mQYPV2WT72r%Ms05{^1ehw*5?azn1O59!|jMGQ_kMJ=$96~** zz7o4!LV!#t9EcNx$eXaND2^vmVIDzs4M~Mx5%arUYy=PgE=OlZp_m2u^Sb9iN!%GC zzNn5gU#NV#2}QSt=^YLm1c5Cq#G*jhxa3!*t4+0L#yZv8Q_57N7skt!*pl)&xa^NS z6-SSX!K0xx3^Aw=E)JzD5vD>B2fTvh%abpW2N5>PkiC(92R71f*mUb;evrvs&k!d| znq0{(4?q6M9-Km?*Jk>r6f( z9F1NOiJh6kMD1J(}W zaPxvWwsg^4+p+2XJ@xR@u*xs6 zY15}zEpxmOv_MdTyM#8CK7zKk4qJKG5_{^2m3H{Z3EQ%DkG=EOE*nCWCcm6MlNs4q zF`9dZ;gS%-_~g_7%5Gb7v!7Jf)zxy!ilSr6B#s=N42hk!i+SoY2-$=8U$n;Onr!dB zgZAdT+w91mCQiM(EU!%B62U0{(oP=4T*lkypZf(@W;g@xBh|0qkKv@a2<{D+OD8W~ ztW$!2&+6s&h0or{>E|JP=lyNAYv&o@bE^<*^Z%Si>q@>&EUNPpCDpNJ{P?F17XB29 z{=hizLWx;p?{VAHzRA9I@mtJ4TZjc=Aa4TiV#2VIh6TE9pw)DJo%CdA-y}G`5mTqd z_rSGW$ix9 zkoY;X8=U)L3dpdyyEHyT98hV+VK8U*495L9PTf%q%@%qbbK^9)C1!E5FQU!Sqo*Cw z=tAGp0CUjO&wkd{tXXYy=gwwx&&g^#90qVOl$F(j+&0+fAU2GHKFf&CmV9Blh5$yW#Sf3%5nD6DU;wRVc-;hv2#p!c!;27q$BU z2Wq!1`;6Up-(4<7B(9eZ=G1ncqQddiMYTQt*nPB>LEI3*G>?2tf{S9d)zC&~8_{b< z8iKERdn{bf!^3c_K#UdvyqkUhLEY+<*UR zIF2r|zI8&Zg6oEUs`gTUP*@R}^ev;YrT?@U@tO7-VZS==hyebn(Vh!4eCapWJsbO4 z?h5fqepv`t+kV)1qe1d|bB`?4tW9~${faQM?y_aWr0rfp24@VBv3c~FOCKgHhLj!D3k&?w}udLh#R^ArL?Hbsw-JrxfNwh z*|_9Z6mdnc*Xh3GL=6{3Nv?YpmxhQNLfWG!iU<L0fF&YDFS?1k(LvNZ2<{AAm0ZMtQV2HTI`Kf{&@C|{a7d4Y#5u&_p!Fiu zR7hHR5TU{inabGRL$1ms0knuEUx7@8o8saZ%jF_2e6geSkWRFqO69NLvSEp+z-3Rp z7LYexogV5a;9DXli=d%;k9kyGiip!OaK%MYI{j7@4wooRByMC08_KEDQi0+d8+i{! z3{W%kyc~J_TR(0W-2w{@p}rw5j(i9lU=Jar6AG|rMN!}u5aN&+A=HCRDuXOUlH2oz z-pd~k z0oUQs3epTa9gA}yQV3B7an@!-$U8+4cYs(cB?gi&nQgBlwo2kqP-i0SpkxshXM_p$ zlGKBca1TQU{GGV_DDiel#U_jwj+Y1r5LcXI10`KfTv5xZb11LW{}8vL&=BNDc~Aib zJAt1IHA#pt82|O8!ndcXdQ99@pD~aVSu%M_fNH>tprSlFK|>pI<&AGPZ{pzahlxUY z64E1aA+cN)zyTwLR^q7f>t5M-6wzKXfujZ;8&V#Mjtz;T4ATx0eF|I~*jda()L?We zLMXf1!|kE`IrX1Zhqsb@tIQ#smoc$=2I5Z=B-BpQa~)2M8w0vy_()l!@2Kyba5*2@ z;5p7A7oV6&44)2~h4!Ph#k593Ro|0RqPj6Qv={}V^KOA7Vp8byPUuP^C>#W-yer61 z3qh~snpf{{N#+2YKx4lZmhoy6b!yIBr1Ush%ew?iHIvuy)G0P}`^rGrn7}Ev2o%W% z$5T*v7ALXOPj7#1?n6*lAc znfB--_t~0zmP6d&M2pSd^-MoyXy%k~^3j#MUh=rTeYagG2(2s~ng>#qO zg|fnX?q2F(Ri0J|iKkDWwr|a8vq>Mwgcfuve9eT?}7*ef#8XI?OqV`F=OEV%cLJehIe28O`HO@+5|S;m${np2XAeL z;e#>GraYN;yk5dmJZRJAPqx)7Z?mVLe%O{Ry@eCwYQkN+<7+PH02$kV-~@=nL~eTM zhDdPQhTz=Gm&|#}46|uAbOhYDVu3yW_(LFhOKtA#seXF=2}w8v=|0%8YcBO(W>w`| zZ2QL`Uz(TX8zV!ADjHv7_pe-NPdxDmgdMIJh4V$?G}klY;@GqI5OZfG2*M`&XamT3 z0)MpfU%N)Gm(o86fBG|2ZzJXLNtDUuBlnJ#iH$GT=&~{Q+Whh*Xt)=mu4?uaf;~yg-7|J=*kZcr z@F8CzcGyn(X&tFv--H$vLMMX3%{R?P34P2F_=g^!V;`;GWWVv-e`Wva-xu1m&pZlo^RWHbuYJq5ZrWj| zPreJ$_Lm*Ol!%rdUwf0niGx&gl1@p*L82NGf~+!?Q6`DZy!ZZQTfbqOz4iJ5E3M79 zKm6Ta^_AAi}t{N>L>pn)Upp4;vH_cz*=b`gF+x`?m6-Y$sC zFTc9ZjyDb1|MBlXZ(sQQr(85<^{OSdbH{eO*izv{2ysfReP^S6^E>P8(4N!wV?S|^ zeff)@wnrYm+fJQ41x(foxbPl&;4UZRc0#Zb2guL=++W$0*+urVKlv#0)P2nD>MIbs zA>>@VFl4{*bE{CQx)83sF>sh{u%GSWP_VW;67QoqZ2W{R;oxR|M zEN?$ndChHaz*+S2%e(EayJp+3|N2h@XBylc3+&99Q}%nm_s6z%>qYzPzviHlwfo?~ z6Au5YU;SGfH?|DI7xfNy7e-9_m#kgOges%ju!Jb7kXJcm{CrBud<9ERXniD@W{$fD z&AP-TVFajDRziP{DP&{D7}5V~AN7S%3gr;w+Rf&8@Rhqu8wHTv?vMd8@cm={Mz6h$du-zUPx zxbpEXiZZU8tF$3{Xl@e1-5zg6K@_E{A8{6mqJXOmv2ToGC?beUn}^Tc$^6gDbEsdn zvFg&D+(B+CN#J4#%0t;-;3h5;NAyw18Pd}sO(?U(W(K&CvjTCB5*?spoH)bUBQ6g3 z+g+)t@V*d5@$`hmJ^kb}=EmIwC<)`%%y(?N5Xo0A2c=j%ILp>OgouiZIfHAm&Z0de z;Bf?S?l>K4D_~-RmD-|xv<^$WM}&>GflG*@w2}M3u`%pKs}RQ9GwG0upED+uy=X?* z6;Ayq^U(fo=dHc8_L?mF-gh!OH-yLvp)Y`%?+QfeBq0>pfUqGhfq`yBI3#u;1VxRm z4TZmBPE=;Hnku+_;z032WP=7Ff3+54pbl(`~oIFT_;+O zC`t(@ZPMwWu*zp&CyI(dlDaQ)e=(G7AvY2o5wSpvl876zJnE8p`qrWh=Y(H_nxvTz z2Zxoaj8eoxeBi{a^66#Ilv57wDEB~#l3$MT;;DwP$WsIZh$w-uAyE`My;@5(>x2y` zfALln7A21p(-7dqDO+oDX3c3dWU)`r;`UMV3kIsz3D^ z!I1J8;(0z5Ux=&v34{qL!*Q4*Zm@t>aMJ3^Lm_NP6h&#P{w0bMm527S@fn2m+X)+N zi2PJlWl!43H?r6nN#Rk}crw+1C9}~={6f~VfkG+dDWg7;4ybVY51~}V3~WNjSc<0* zs@P0=`o6BoLipsMHyUmPlpBq9qv1w?57m;23}QHp-0twSSvI@?&JDy%yu#9zCZdH- z=FgtCyrYPNNdL{u`8GUd8smvmX!?^I#Zh0XtpnDrp|Uq^gxep&$@05wc+O3MV&pfdT1#ie!!clTPGnSM2TNcb@6MBztNdNO!zvEHzYP8_MRVD>{JtcN43jF@p zpMRE<{RN!pj}Pp&*8`5fkmQMotvvd`3Y$2gic_|J`*`y%Ydmy@18NZ@(k6`a=%Nj> z5DuLte(hJEvHMpq^V7>n*ig7ISHu|V_3?>rFvr6!_uR7&+H7RbXL8!CdI_8FY?!pI zm^{}ZOZ_Mt`mxAj?>lnd8V{VbVNN3R5WP^ELtXSOnoJ7qe&&mh+fz>l7G-hBc%IS# zN7&_G2^Zh3x6EN9ELr}E_QH#=+Y8_R8gt1a=lIiL9&!=FllGOL_zW!OkAO_haaL(@ z;M|B2SIW}cZgx?hsgrAL`)+h9be|tM9oBP}KI`zwV=~TCBT~`A+~l@@89&Uj6a>*KPVdGQK-}PjOY^R8 zg!xr9_-JBX(kASDU!xuA+6~9XhqkYC8^lBA9ZsK?RDIcIlr7+@!7(nTvZd|ooTyE< z?TD;!+Jmm$c5X|#)yBgeA~s_*p!lXusVg=8g@0`iqdjqtVZ2R(oARWPL9;tzq*}MG z+dA}p);Mk0#*VGB26QKs!Qm*~55>|0!+0Gx_NY~XXiu!0h{!@UC+rh_JB^ z?@k+Axd={=X|Ai{2>La8`-W{HN=)l(D>%zQ5ha`!e}MV!M3dxOGuB5sTFX zBaOJ(KHjp+%cg5}XU(2r8`c>FMQ%yCeIc9$oe({bT4U3B>KS1{h(=*P8Dl$k9)vTX zAEcxh;^?52f+*ehFrqS3COhYxhyhYwDz2Jn)K=%3o4IjilYRW5#M*Y*i4%7^@m6p( z-qn6Kd;|plBRh`ni^gWS68Z{k&BKdq;mvcre5xPUwNX6IY9^vg6TKnj1 z;YR!XbBpZnzPZT}KM|o$pKV5u$7$=m_@Zt6U*t;Cv@y z?S*LjJ|deI$F&E` z?S0oCU4%;qP1}KZJrjcKS1D-KP63Ek?RUK`aL8mNdM3%cKOj6{@XLm{UM{l!xpDLr z0mm?D*oi$EuL_Zq|53gJlw26$E5?m}={CrwlZ78+h4N?JLkfY$q`ZeG%$DF}oWd(C z;D#jcMfuF1syEd$+c+V!>PyZ!fJ0ePg-t*drIuS!B#KfXQGSB=Ibq{U5Jl-| zM4vH-H7G3&+!_#IUB?DE280da7oG=STZpWG9&az+N z`HjxeKhx&&2&v14A_4w9r~HO8vRmO#1Pb2NgVsKYc}PL3ly^r(p&^3DQcSNuaj5hR zcKLyVxFyAXLTvdu%&;1gqWAldTZfa$A74r3sNMA^qjnd5D$F$4!&~i+=m~kEzU0p% ze{s-Nl%dp=FV+XxN%G&ku_b^F8LX6Z$ zEUcXT;Ya^oNGue}5oWzv>|GQEHb~uyQV#N5Dp3?R0ICp)F*wHtgpIBXEf6^<2d8M- zF$aZ|l!s2{Wu-7)9%YDlQSoPfi4>}`kg4*hNY}|B0tdzk93b%-F3^j#DM<)Ls+D8Ft(0|a}Lkia=exvDPxKfLJy z?&i*39@?LDJ?->20X*9=Zl(;u67odHMMOme3fNqdb`b{+6Nu|d4dttb*jVG->_ZhzPb8sgaC(TNaG)Un~^_I66zO4&*BrW|M5L1lLj zJYyZ{orKju8`XH&QGK$GNmNSxLzru%`$Iq)HOdWuSH5o`*r?z~b!>xhuxL8BqKL48 zoG_D%E7etCbu`r}9A9}&5E6E6vAhpIq(jhORzO_PiC0}iAV7HgBspU~auJnVn6yAp z{U(*^bHJcsan`0X5<>)E2I6I&2q!x>Tiyr22NBF?oa?&cVdQ)z;-WDh`U2rAt8BQQ z&DP@E80T!>kDnifxG{PyDM=x;6F41@j_^M?)-Ue{$N^(1)7+?URqh$D-MC()64XvG(Dm^Nw}vB|sM zA;Dnr%PQatA;*J+t>jcxIz2=zkx>nb_m++L_?f#gDB6O z+0#Kx%B<~5yPZ7KYFj?olPv8+R)%K(+gE*#)7TXtK8s=BMh{D3Btp}UEWFZ?l0Vf$ zA$28K5+xA}wc2|eW!JN&{Vx08e!0`W`hS0$KFM5$4v!M_q^w@^B~C9_z%G6(;j+Xq zwPy#&UMJin(m|jK6VfC^sHzh2J@k~QOjV?%M|u2J!vF5?{VlhAUG(|W^#Q~T|BHTy zt(dqjs$jF%hD+P8?4g7TpXai}m-}1oVAl@&=*kalOFL~bc!jH7@393HOKefq-PTY# z%f(M5S6m9m&6dv1*4h7|t!q1HC;HCN&j(z&=vZ`cNHv*8gQrCU`84G{TdPR;Q_2}T zZJ4#orkKnR$TVL>sahBnx#LUmbB%}r(jhZ|vd30L1{z%?p@4N& zW}M1ML_kxfxV_?>skK$}oDfPey=6=+uPVk3B^E(XpV0}c`my#3ot-_DwN*@eiN6qB~Ms{T^I`HDuHTq$$jh|Z>` zpS%}je+ryZi&1L%OE!JRbi^U*ZR4pe_5<{HNJ*tkcD{qq!CX29QCjpi0TnhOUw>6Ff)_3JjqY`~Rp8PT$KpDWZpiX+`WHPuzjW784ssG)sI zPzH^*9@mP{O&e+*Idu4>?LT9^KEFd$ z5CKSB8&Wzdk-9HDyUK|UY8qV|ta?}dOTUH2ipnF-nij%~8z>lVr(-^nZY1e3(wwP$ zqbC2{8AB_A(*71c$v_tib}bT$WQuH zF=NOKlq@_Cnstw1eVltt1qksYl;Dze6|j(sd@%Z;pg~-McGuhqkz)||s9!^z7=c>@ z0s#G2-$kep5XVN1i=x!on9`Ie3VVn)#80GO<6JwpqVy!UqA({5A$P(Cdk4+YzBdxS z75`MBvEucP_QYQf{wH_xC=^_lOdluhzCm3r%QzJ-q(Dv`31J2pdjt98ArEm`%AtBv z{fTQsBo2LpVUaiDc^bTB_Xh{%FY!`7vL&YOqK-*N*xCj0cq^V{H5wBm>IPvEsx55Q(5X6vaxu016=4pkW4) zzZ>X+O@%kDgCj*s9zTJQU17t^?IAX9{+0cS?pc>;3k6U)9NAY)4aCetI3X$erZWdl z0NzacA~xvSFK)@{UY~FJKjr5~vnHe3FB;3K1J2QNXiU_l|s@Pb7S$HT_P z)J6;O2@@ws;|W6TA>V`+yo`wj%{aIvOte`yA=sDL#I_(p)6M2v%G+G|C&UbG($Z!E zoA^1%oG(YI+5ns*r}tb$JO|)&H4zN!0o#Zx^hzKV-VqX zF53JTapHH|Vm9F@>5E1j6RYl8Y)1|s!3pJVY`isBW{t{~TQ;2r>*fNLExXYt6!j%H z>Gu#zd0;V5!^%R92zYc+T*<%dq*i(`#Of`AfHv79%>?F$`(X7x-+UJ0#tWp2*b!VC zu;BR337`Jd{k90gP?|WRjqW*4p!cE+WY^v!2_jL#%>wndY{^X~?hz@}lMxa!W%5K@ zzI=(d)0U5pQsr>^aI!FAY>hqf*lJEa7vvB&1pmz0^R{K%Ui)C<4v%7{k9moDb%7hnDW-6TQ+nz`bz9tDD{L1=EuA#8|Pbmq)C*9Wrkqg^gm zqwQp*5 z?VV(#dqCn3bQQu*d&*Aq?6JQax@Zp#K5C0ASK5R^kT=ruwa-)18mO8L>H2RnuS6I3 z&A74o(+M7@S{DW{CR5DlV<@y2!1))EOGLX5H|?_5Uw_+P{La^*IxJ@WVQ(WbD&~xa zx$_)p7J)}Pc_iwTCAzYKB@cVK@#56!v#zvLx<(#+@E#OhPa_{cq>n7BZbO|N7gwB;`KhTp^ttxNb7{rM_ z>$^6p%D=?kgrrLZM`cq!rEF7W=_m1i=Z0e48O3O%A*xFaqo zrcRLj7W>XOciYeZ(kik^eNqlREKIgy#Y6VgryqCXhKLf4=%Q#k*Wz=J=6WF{>HrF_ zs!=TU!_crOHZCQ9Y?n1x#kx}&Jf7Wb4?lE2qG@X!dA|(7rm+eAZ>*6bZpM7%vsV$Z zTEIPDyj3?q<uC`qJDa#vM3O!j;tLg-r}0ZLPZZOxO~S1L?1bte%a-I_9G zDg@_Dn)D}#Xd-w>5wk>GLYe`#Ukgat4(qZ4iT2#e2(m| znTkk7%mtJFqQ3+%+0Yq>B%Hreabo%@r%@uiQ*Pe$6(Xg7vvCt!Qs_+Clk{TT1jJR* zuI-@IV(j?7c4+x{t#}4P+~B6TA%2dHaM`~QGPZ$l)2U%6a0G-3FG33}O&|M!BejTj z2-y~4V|>o7D1iq-x1x0DR+M&bIJ$BoBN`&n6A?8Ac%q&StsTCe(DC#q6UI$$t=*aX;KfWikOrvZkMAQeWgtj zAQLO`bAki#@ebr1;2=684IG+jRJvaBCc=ye7Fq*cd8l)zXdY56gnASg%%C`7AQn-p z!~{+vh7wBeIV28WNj~+;WI1EOEz^f&58omhX^xV9M{()sx>s@K=*j0PM9)e&+-Vc4 zs%?BF7m1X@xlsuFwr&C`;3P`o7n+dNLVj9~D9T_rgbg9EEPQIXOk?ZAO{!s%$_qY8 zZv7*;=r>b_krvgkIVRCK&-YSzeu#rfkM$o?-rKtuIqNaP<@sL7M7faFVh`)4jsEO5-278f2 zT|BS2x!4X3BA61*P>>uW_?`wY5e9(e;y}9YRV%ZXo!B7zx|t?k_sZzQPw*l@*+b~j z;Hwm#4rwVytUrG%U7ie4Lv{#7Wgr?-$|~iny`1>;L&X=I(Dooo8mykayRt@7?g2U; zi<@^o!S#M5cr3$QJy%}Cq6d4Zu2%F#k9zG-L{ZuxZ1iPBQD_&nk=iF|qtN%1Cheg% zA#MHXKc{L{Ks||x)+CqFIX4cjF2>IyX-H|iJV$~_p%qCFp-2E!skXamNWaw9ve%z3TnE$`rN%ip9J z(;9ADk?Ye2hZdbYmCZXFw~@pPxR~51{Y<~dMI2@*S42&{;jq)K-9;E?KmI&7MvkRpG%%JO8ppbF8I_YX$dgM{Il?$kj}nH)jS)Jg0*| zXjTbE#FBl$fAM>7`<04EPM+h6Q}T}4TRX;196L&N_j@|xn2^zCQ&;EJbLE~@&tjL~ zh>~RYN`91=Y<}dZs!b!&B1W8PJdZZ|QxGURd~<&Y`@zGf3C&Ffiu4Ia3O#r4I|du` z5pH2=bfiff4^lF?V8J{HJX8F%If4*}g02vhGCwF$^a1suCcE#U$LwK5ZKh3a;6{g} zAK;^tXc0BGY~5j-&;g*lwzYS2(#rdp&)BJE#6eK{I=!x()5iVQ-6@&% z0b8_i9%TuHH?fc_uWRnP4ZSMuc5u&LSnX>eY|L>M^9Wx^lJo*>fJ^21@4n3m=2<&& zu7i!lkj<>Gw8MuE!qIUVg3^E!cf?U6Q&z?mms1*S)yl=z+ud#l_H4lwNlMve~V+CvO}`bhq79vCQg; zCxS{~TlN*7x(Vx0SPP>)Iw2GLF^U>dT51qY(9l5KPi_&lTbOPUXQ8i1fvqE> zz0Jhe*ieXaYdqWvBB|?U^B`a}+N-avgQKGnZ*A1MC5Ed<^Wmzz#EJNEt_ywci!0$C zT0$S1WZQS3WcQs7Fuh&2Q)k=l$3FLn--@A}g}oOR**qhp4#K+gB=c=Yt2{i6PSHwr zxWpW{4{@wuE;9$B(*(piO(;z~lUMAStOGEew_em$gu|TLciCQ)NN?Z1!%iGO=G+W@ z=+>YLD0P7oGh16wn5y|fb9%~TYc#QexU7A53q;KZyYJpR5Z7sd>!6)_I%mg@pCo1r zJ2aIem~gT>8P0=SZ4FnAo;Z1yn?=^!83^YGQ9Qcxu4O2^tYtaj#8`WrB%ZHH6eshY zxQC^Ret|{yK16N)?Z122Zi6#V<9XMvz1%pm0m2Q62K$Q{xL8qBRB| zS;|rKsK5bMgc#FG`=tG{p=Aqs(rqdq;0aR4?55=W^htj!g@mf;E;juSP|Wbpy2pc zLckE=qJT9eaFl@W@u#!{fdhO^_22}Jke1e0K@a)kSBWLYt^9->g;FXvUMAe-?>H)X zP)b4MB-E9-c3jD+h#Mku@Khp(QeB1e`>GlUT`(f>S6B#3qjL;s)31)jOyRepIAH_j zp=c!*vLXV2FXEa>)Np$HLoQSnHyKNGNZz3mqOUTZV?s%SQ>KICD7%mXCu*qjDJGp+ zh1RVug)E?Dh&YJ2Q4GgM5Jl0gDA10`w-kqVB8t-QN>H^r3TdV+OpuN^3t5h3Q|c;D z+9-XDPCxuqLH;QMhZ6?4!mto`mI)8*N-rG_-};XrRT>~{Eih?)_z~6R2OI9({pkgW!1p!7PHnRmy3O6CtSE zK^Bq1(l+b4T)-(a?C{j9*Z@UQ4LVo62tXw-DCBW56rEPk>71*E_EH)mKxMD84WdH* z;@F^lIuJ$CttcYmc-=W+gLWA~*iha1pDN}@ZE|3$o`OTDvFjw36Zbk{{|9>iqhymd zGM%`=<~xisMtJF|Iwmyl{}fi^RW~~v<#v!gAR0UOBeq-3cxEiqPn-y{9Y4Z}m?igN zl!=0!XKN}MGwzUq%a`UL!9IJ+@^-?pu>&M;?@{n#@*(&|_)G80haaK$fXNQ|^WmG( z;SpgA7JXBB9v%x(038~6hc4kpt0!Y6n~BkF;!4=L`yi+E>+FyJ%l8P>%+tm*o@cE6 z=5PP3vwF{lgCHWmcyLNtf}+p{o9NsYSE!0StGgZIEJPg5N8;3w(MImnnP&Tt+eCin zx4w?cWge8Ia%x8f*fG;ps!Ob*3i)i2!*K=Xqy@@GY)QgARHfq8|S_EH`#07|5MylKEcnoBbWN@ z$o}&Xl_YAz)p8ItMD!6MMSVxei$n+le3kbfX5!p%tzHrR|6!^8C}K;$)dU-hNQ&5F z*#WRUg}agrcEW{ouJ@E}Yg=#My-a`WKSm!dg`n}A&9Au4Zmzu38cJqB7=f_CpERng zk7(;uSui$?JzR7;wXbuF9q8U?hq|7(L+I%cVWN*saeWD*O%Ov^(TK?!r9TqapI~MG zGzA8K!PkaY^2d5a+_1cfguhm8<0W6_AU~O8j*<=r-KOEDC>aM%{KIAVBpgL8Ai^S= zz?Xr@$+w^1ZO0GowG}JxL^1AS#sI{i=6ri|ZHM=pYPcuIBfcgPh5!6JUn5ol#J;<{ zZ;8O^C-`J9OoLKN%zg>r8iW7(*`>DW!$I4zEnIu58+{_d?brU@QxF_)Lcc@}oDz!| zbBrh81Of+j4L~6#B6{cv@HIvlPbXX9AZbMX5|=*0-6ev~U;X79_?6h>Pu>BC)I8>f z@emBVZ0(!pn9rHxQv_oICUqv*;x-VQa-O~X-Ir|5eM{Ns-bMUzv~!WY^6GmKcJ_0a z6!SuhYVA=4(x;?9LOL>zq8DTBx}Em&i{Hn+--#O)?2&v!uIBRsXfGPz1dal^^I$?* zX+JM3L@t&_)s5yj5rlNT?eG8B-y-CE`|&S5nGtal=S5-Ru$?_|k=sSiz`;@t*V;T7 z67RzuKgu_K<}4=^M{bKympx9vszo_$nG=HZ>|5X54*YIgdi!i{%~{ATFf+K$`>?(C z>Q3B4dnlj&1R%JFy!+nG{gML7oiL$X%7^xck%o?^O)bkQF)ChF=Y*i5b@E-hU{eGS;< zOT7@~NlN+R?!AHuVzDI%Eo=1bUs{(=d%;2qrb zn<-8bD*cN7X}4>aGN$uMp%o_PXteObKMNuKosAn}@~id@{{gDovt{Pf-IHJn>W_Xr zl-_Dv;SxOR@8QHAjvE*b3?+^w)0om)r}SCJwAwidi#Qv6q<>Z-W&>z zU4>^Q@i0;zDp3?y9*Q!<0-xd7!?O^FTTxnE6s4b2M^z>f-l|6yTP+?L zt_{A6V81M7QO|I|h#eGB9M!Ah^dK@KP75bG0Q<{&b};sLa6D2qo#wC^`0wjz=E)$_EBH+b_(#ROfRy2GB#CV{79-ZkOXne ze1~Z96M9TGPTHElac}Gyuqsp1cEOtR44|`t<98-D4l&^f(8J_>WMW;@qRPFID5It| z66!SqWt?v$A;C<-#o&x3abOG!A(fsD%_Fg&dKU2Kn!CKcyWr9QIo8P&eJNvPpFGw$ zHne>DgvO@nwiOpqiJ4QfF~JxUKdA3^>V)NGL|r(|iG&RgMi`AJ^zh^zJ#P7xV{Hhy z!@P5h8d$fgz^&x{O~Wj#GdaOVenM<5>ote|K;wger3E_4b3P4YvI1F(^$Qkpmh`cIZV#G0VQ8zY$n7S#Q z;;%4=GoN=?*g=#lzO(LQzbWT5C*e9ZmF)R>WY;?&5QVD>r?@VJv^b3biBKUiCY`#w zDVeH9&rTgZRn%h>H(Y$+h)P34z3VO!@+sMP=^qgBD-cBjh|zviMHp#n~Q*UESCflu5ccDOZ zHOfJQXh|Qev)Vw)s4t0yRz|0Q(@N($@gmP*7ubGrg)wg4y8Iox)ZYT3WQENxy@m8A zcwCKh{{xQxG)}y#lJDTg)`LxNH?qjRB5n-Bz2S}WNi72uUtB$Fl#{SY=hRv(GhCE3 zPX8%8aNr2Y`aZvM`qNK5$Z5a9nQ(_KT{6d3Bj#2FLHED`t`dfe;cO#%t=QYi)T4;> z%Rlq4Q1{bmcdcB4qT9JH!l3+Va~RTUc5VuZg-n7I?Ed>!z@64_JN90LAkpZ$Anqpa zWC+}kKKg*oohKNDh*NRX5*%u6THigjP~PyHDGok?(BEZJ+zONpMv1$eet}#yLLaf zn4sL2>uJ}lxy$RhrgjqRPdT?{t@i6(j~qEoopsySzV@Ek!=&m1C3y#cO4v=TuaJt7B?3#{>0ntexn^^b4&!z&C;lOM zPdIB2aUGVz30W01^^@S3z}7c^?ym6$9+LS>%Hn=W7{;XfnG-g^mqhTDNP-aC(n3U0 z%Bs<`!CYU)tteU#n73RM1uhK{HX5&-LYGqD*a%`MLB~eC?lmgA3)knwt0jWvmHgl3U))IyYWiUW}gBnYeZ z$RA%vz^^<#kMd@da!l%FGlN%z0jc(pdF@mpIp}cBumR_e?1JlfBzZ>JC<_3Y^-5)ubixLrD7tP`2)55cDmYJ0&FOn&)hVMmdwym0 zQu%bq*CmIum&*2vxa@q9CUyt`m%i6@6?f zyDzm`56YcnAJj^pA#890L7ALbq0K(wy<8qy@sqsDD?s#o$?lvAEIMpcyj%q+6qSgI zPw?bhdC*eoBOjuWxE#D9M%0Ods9v0~AW;;EOHp~K%C60n>zhC?fs3MuV?zj|@~S+? zM(_;!j}qlyokCXs4R5c{;79N9I#hn0dqZ!zaVk{EiE%mHFXf!{mY`Tn954Y6^#Kd8 zw}TUzE``ur75N3+cHo-}cdN)gmrg*lB$SRmibOci(zxz0%Uh` zs{m!)LXtAIZ_{Mwdw_GF5p35h4@WE#)3&Dq-MKk?9eE zDYq7TTHD}k>9?^J!+xr)IZFtYI4jPayc`C>qzY%3D3ed*b!($#r&&Hiri@1r_D{Vf* znx$yTpE{J=sVBk+_R>mj??F^d`Z%_CyhZ;yjdIcn)IVDL2RpfGf8(^~CeDi#` zpV0BZTq;7s=Bq2a$m1!Q4j`N(~&|ah}__d85lk+B6^cxE_;U%DdOzdiw(x|GNLaJGlvI0b}_b zqBHHbd(UCG2q1hyTvYzWQ6PfF$x}`KRvUSj0=n|kggy;j8-Ae$?o{ex?Yo<~)r!5A zxc9g^^;iCvpKw^Z=|*C9gRH5|)J-v*8~G5h-u}TxxD<*Y5?z5%i{3H_G!hxy1D9ei z`^<@mAF2kVpQEy}&|Z6Go6VY8@AGACZH+xnU(ji>*!ZQZN!Ra6jL+9B)}ChOn2L(? zD5$;Y`Nabx0_qjl1)_SncnKn(n*y(|^VIDbI11G-wSK|QXRp5cF4y#S+0E!2nmZQ} zOZtiOr2|3{Ha_@Z3s<`Cvq=r;kzsypgvh2FYoy#X%uvkx^m|X2xlN)cmoJM5C(fQB z*2XQ&eI4F1C7-QC!j{xbmCvmL;~3mzFOz zvET`oe}xdFsgO!5yi>m+5`NJHSo)Gz>L8mV6Y*b zh9PhixHu1aQ@-F>2$M12=*e)71R-K+;{%tbV>9B)LnV?znY~g36LzX0L{Xe$BNCYK z#V3=uP-AJJ9-ALJMyyZ0lgEh|@=NMbb?Dq1q+2Y}9@tbXYHPV=1`Zey5GQ6RO+ZLY z#>H0>;)q}lB*dFA$d&Z~n+ia`hB#_a_Ew4JbpVK{A%cb9m;kaX?vH_%7LApBvzNM|=h4qc&H==TW0C@M(l88BQe3|T@@22HTYGKNu=j;0^!0IGT*X^;=g{8qK8O7zcIyZ%2lz7E5_P(ZXMaaWgiG| zr>$DG%x^m?VQv)n&?%7aAFTb*zVq$xQAr?*LnpW;q|V30lEo;vg>cvlr|y|Er$OA? zL1-K8#EG*Yz2|*il8D{fcemQ5D}DClr&ckZ#zJTT+138Z5N~)HdFRS9yBk8AZdnpx z?--nN>(=j}&tLM}oK8r;MQJ}olV)^j>_T**5<v44uag)W9LAg z?^(Un#n!a`>sr_S2anlb{>|IAcbfcZW7tTb(-HgifHH&JEMl)Z|K0 zKi>E@TziN~vU!?2cZQ1(_OKt4o{DYcSK;=eo1(X;13f0(lCuA#{YmQv6ec&@>bvG6 z8blw55V3aMetYKAciR2;uK<>~{2*XxeX~vWXMb6SI7=(-br*!E>8@a3#De3;Puol1 z-@rApdk80@LYWh;&H?W<#2bnFna$uSbWRM1$$WeiT}x)OcuKf3w#jy(�sQlIzeA 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 + +Harness FME supports the following monitoring and analytics sources: + +- [Amazon S3](https://help.split.io/hc/en-us/articles/360053674072-Amazon-S3) +- [Amplitude](https://help.split.io/hc/en-us/articles/360046658932-Amplitude) +- [AppDynamics](https://help.split.io/hc/en-us/articles/360020898371-AppDynamics) +- [Bugsnag](https://help.split.io/hc/en-us/articles/5709939011085-Bugsnag) +- [Datadog](https://help.split.io/hc/en-us/articles/4822553169933-Datadog) +- [Dynatrace](https://help.split.io/hc/en-us/articles/360059673711-Dynatrace) +- [FullStory](https://help.split.io/hc/en-us/articles/360045937831-FullStory) +- [Google Analytics](https://help.split.io/hc/en-us/articles/360040838752-Google-Analytics) +- [Google Tag Manager](https://help.split.io/hc/en-us/articles/7936008367245-Google-Tag-Manager) +- [Grafana](https://help.split.io/hc/en-us/articles/12397463150861-Grafana) +- [Heap](https://help.split.io/hc/en-us/articles/360035207311-Heap) +- [Librato](https://help.split.io/hc/en-us/articles/360020950431-Librato) +- [Mixpanel](https://help.split.io/hc/en-us/articles/360045503191-Mixpanel) +- [mParticle](https://help.split.io/hc/en-us/articles/360038306272-mParticle) +- [New Relic](https://help.split.io/hc/en-us/articles/360020695432-New-Relic) +- [PagerDuty](https://help.split.io/hc/en-us/articles/360046246631-PagerDuty) +- [Papertrail](https://help.split.io/hc/en-us/articles/360020700512-Papertrail) +- [Quantum Metric](https://help.split.io/hc/en-us/articles/4423968122381-Quantum-Metric) +- [Rollbar](https://help.split.io/hc/en-us/articles/360020700732-Rollbar) +- [Segment](https://help.split.io/hc/en-us/articles/360020742532-Segment) +- [Sentry](https://help.split.io/hc/en-us/articles/360029879431-Sentry) +- [SessionCam](https://help.split.io/hc/en-us/articles/360039246411-SessionCam) +- [Sumologic](https://help.split.io/hc/en-us/articles/360020746172-Sumo-Logic) + +To learn how to add a monitoring or analytics source, click on one of the links above. + +## Deployment platforms + +Deployment and serverless application platforms simplify deployment and hosting for your application code or infrastructure. + +Harness FME supports the following deployment and serverless application platforms: + + + +- [Cloudflare Workers](https://help.split.io/hc/en-us/articles/4505572184589-Cloudflare-Workers) +- [Terraform provider](https://help.split.io/hc/en-us/articles/6191463919885-Terraform-provider) +- [Vercel](https://help.split.io/hc/en-us/articles/16469873148173-Vercel) + +To learn how to configure Harness FME for a deployment platform or serverless application platform, click on one of the links above. + +## Development, change management, and messaging tools + +Development, change management, and messaging tools improve team efficiency, enhance developer experience, and help effectively manage permissions. + +Harness FME supports the following development, change management, and messaging tools: + + + +- [Azure DevOps](https://help.split.io/hc/en-us/articles/4408032964493-Azure-DevOps) +- [GitHub Actions](https://help.split.io/hc/en-us/articles/24994768544269-GitHub-Actions) +- [Jenkins](https://help.split.io/hc/en-us/articles/360044691592-Jenkins) +- [Jira Cloud](https://help.split.io/hc/en-us/articles/360059317892-Jira-Cloud) +- [Slack](https://help.split.io/hc/en-us/articles/360020997851-Slack) +- [ServiceNow](https://help.split.io/hc/en-us/articles/5524203735181-ServiceNow) +- [VSCode extension](https://help.split.io/hc/en-us/articles/10731776599309-VSCode-extension) + +To learn how to configure one of these tools to effectively work with FME, click on a link above. + + + +## What else does Harness support? + +For information about what's supported for other Harness modules and the Harness Software Delivery Platform overall, go to [Supported platforms and technologies](/docs/platform/platform-whats-supported.md). diff --git a/docs/feature-management-experimentation/_category_.json b/docs/feature-management-experimentation/_category_.json new file mode 100644 index 00000000000..0333faf43a4 --- /dev/null +++ b/docs/feature-management-experimentation/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "Feature Management & Experimentation", + "collapsible": "true", + "collapsed": "true", + "className": "red", + "link": { + "type": "generated-index", + "title": "Feature Management & Experimentation" + } +} diff --git a/docs/feature-management-experimentation/fme-support.md b/docs/feature-management-experimentation/fme-support.md new file mode 100644 index 00000000000..2f0efefe7d9 --- /dev/null +++ b/docs/feature-management-experimentation/fme-support.md @@ -0,0 +1,61 @@ +--- +title: Contact Harness Support +description: Suggest a new feature or log a product bug +sidebar_position: 90 +sidebar_label: Harness Support +--- + +If you believe you have found a bug in Harness Feature Management & Experimentation, please create a Zendesk Support ticket. You can contact FME directly or on the Harness platform: + +### Contact Split (now Harness FME) support directly + +You can create a Split Zendesk Support ticket. + +To create a support ticket: + +1. Send an email to [support@split.io](mailto:support@split.io). +2. In the email message, provide steps to reproduce the issue. +3. Attach any relevant screenshots or mini video clips. + +The request will be routed to FME support engineers and create an internal Zendesk ticket, which is actively monitored by the Harness FME team. + +### Report issues / bug on the Harness platform + +If you are already on the Harness platform, you can create a Harness Zendesk Support ticket. + +To create a support ticket: + +1. Go to [https://support.harness.io](https://support.harness.io) +2. Log in and Click on Submit a Request +3. Enter a meaningful subject +4. Provide steps to reproduce the issue in the description field +5. Select the priority level +6. Attach any relevant screenshots or mini video clips +7. Submit the ticket + +This will create an internal Zendesk ticket, which is actively monitored. + +### What’s Next? + +Harness FME Support team will communicate with you directly through the ticket to keep track of updates. + + \ No newline at end of file diff --git a/docs/feature-management-experimentation/shared/OutboundLink.mdx b/docs/feature-management-experimentation/shared/OutboundLink.mdx new file mode 100644 index 00000000000..80bf1dcae4b --- /dev/null +++ b/docs/feature-management-experimentation/shared/OutboundLink.mdx @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/feature-management-experimentation/shared/_find-sdk-api-key.mdx b/docs/feature-management-experimentation/shared/_find-sdk-api-key.mdx new file mode 100644 index 00000000000..42229b6ae6d --- /dev/null +++ b/docs/feature-management-experimentation/shared/_find-sdk-api-key.mdx @@ -0,0 +1 @@ +In standalone Split (app.split.io), the {props.keyType} {props.is} found on your Admin settings page, API keys section. \ No newline at end of file diff --git a/docusaurus.config.ts b/docusaurus.config.ts index aae83be967a..0c0484d92a6 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -131,6 +131,10 @@ const config: Config = { label: 'Feature Flags', to: 'docs/feature-flags', }, + { + label: 'Feature Management & Experimentation', + to: 'docs/feature-management-experimentation', + }, { label: 'Cloud Cost Management', to: 'docs/cloud-cost-management', diff --git a/sidebars.ts b/sidebars.ts index f59c93e8e47..c8ff29df02c 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -104,6 +104,18 @@ const sidebars: SidebarsConfig = { "Learn how to change your software's functionality without deploying new code.", }, }, + // Feature Management & Experimentation Landing Page + { + type: "link", + href: "/docs/feature-management-experimentation", + label: "Feature Management & Experimentation", + className: "sidebar-fme", + + customProps: { + description: + "Learn how to enable data-driven features and gradual rollouts.", + }, + }, // Cloud Cost Management Landing Page { type: "link", @@ -866,7 +878,108 @@ const sidebars: SidebarsConfig = { }, }, ], - + featuremanagementexperimentation: [ + // Feature Management & Experimentation + { + type: "category", + label: "Feature Management & Experimentation", + className: "sidebar-fme", + link: { + type: "doc", + id: "feature-management-experimentation", + }, + customProps: { + description: + "Learn how to enable data-driven features and gradual rollouts.", + }, + collapsed: true, + items: [ + { + type: "html", + value: "New to FME?", + className: "horizontal-bar", + }, + /* We could also do this way, if we want the tiles (for docs) to show on the Getting started page + { + type: "category", + label: "Getting started", + link: { + type: "generated-index", + slug: "feature-management-experimentation/getting-started", + }, + collapsed: true, + items: [ //{ type: "autogenerated", dirName: "feature-management-experimentation", } + "feature-management-experimentation/getting-started/overview", + "feature-management-experimentation/getting-started/onboarding-guide", + "feature-management-experimentation/getting-started/tutorials/index", + ], + }, + */ + { + type: "autogenerated", + dirName: "feature-management-experimentation/10-getting-started", + }, + //"feature-management-experimentation/sdks-and-infrastructure/index", + "feature-management-experimentation/fme-support", + ], + }, + /* { + type: "category", + label: "FME Feature releases", + collapsed: false, + collapsible: false, + items: [ */ + // Release Notes + { + type: "link", + label: "Release Notes", + className: "sidebar-Release_Notes", + href: "https://www.split.io/releases/", + customProps: { + description: "Learn about recent changes to Harness products.", + }, + }, + // Roadmap + { + type: "link", + label: "Roadmap", + className: "sidebar-roadmap", + href: "/roadmap/#fme", + customProps: { + description: "Learn about upcoming changes to Harness products.", + }, + }, + /*], + },*/ + /*{ + type: "category", + label: "Harness Platform", + collapsed: false, + collapsible: false, + items: [*/ + // API Docs + { + type: "link", + label: "API Reference", + className: "sidebar-API_Reference", + href: "https://apidocs.harness.io/", + customProps: { + description: "Harness API Docs.", + }, + }, + // All Docs + { + type: "link", + label: "Show All Docs", + className: "sidebar-all_docs", + href: "/docs", + customProps: { + description: "All Docs.", + }, + }, + /*], + },*/ + ], databasedevops: [ // Database DevOps Landing Page { diff --git a/src/components/Docs/FeatureManagementExperimentation.tsx b/src/components/Docs/FeatureManagementExperimentation.tsx new file mode 100755 index 00000000000..d64ab2da0a0 --- /dev/null +++ b/src/components/Docs/FeatureManagementExperimentation.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; +import Link from "@docusaurus/Link"; +import clsx from "clsx"; +import styles from "./styles.module.scss"; +import TutorialCard, { TutorialCards } from "../TutorialCard/TutorialCard"; +// Define the cards in "***Data.ts" +import { docsCards } from "./data/featureManagementExperimentationData"; + +import { useColorMode } from "@docusaurus/theme-common"; +export default function FME() { + const { colorMode } = useColorMode(); + const { siteConfig: { baseUrl = "/" } = {} } = useDocusaurusContext(); + return ( +

+ // + ); +} diff --git a/src/components/Docs/IncidentResponse.tsx b/src/components/Docs/IncidentResponse.tsx index 414ddb8ee5c..5d5420c12da 100755 --- a/src/components/Docs/IncidentResponse.tsx +++ b/src/components/Docs/IncidentResponse.tsx @@ -15,7 +15,7 @@ export default function IR() {
-

Incident Response

+

Incident Response (COMING SOON)

@@ -52,4 +52,4 @@ export default function IR() {
); -} +} \ No newline at end of file diff --git a/src/components/Docs/data/featureManagementExperimentationData.ts b/src/components/Docs/data/featureManagementExperimentationData.ts new file mode 100644 index 00000000000..40752aff8cd --- /dev/null +++ b/src/components/Docs/data/featureManagementExperimentationData.ts @@ -0,0 +1,77 @@ +import { + CardItem, + CardSections, + docType, +} from "@site/src/components/TutorialCard/TutorialCard"; +import { MODULES } from "@site/src/constants" + +/* Define the cards - start */ + + // Docs + export const docsCards: CardSections = [ + { + name: "Getting started", + description: "", + list: [ + { + title: "Getting started", + module: MODULES.fme, + description: + "Quickstarts and key concepts", + link: "/docs/feature-management-experimentation/getting-started/docs/onboarding-guide", + }, + { + title: "What's supported", + module: MODULES.fme, + description: + "Platforms and technologies supported in FME", + link: "/docs/feature-management-experimentation/getting-started/whats-supported", + }, + ], + }, + { + name: "Help and more", + description: "", + list: [ + { + title: "Harness FME support", + module: MODULES.fme, + description: "Open a support ticket with us", + link: "/docs/feature-management-experimentation/fme-support", + }, + ], + }, + /* Not sure if we want these because + - if docs are well written, they may be mostly unneeded + - if they are needed, they should likely be close to the related instructions/topic + { + name: "Help and FAQs", + description: + "", + list: [ + { + title: "Troubleshoot FME", + module: MODULES.fme, + description: + "Troubleshooting guides for Harness FME", + link: "/kb/feature-flags/articles/troubleshooting-guide", + }, + { + title: "FME FAQs", + module: MODULES.fme, + description: + "Frequently asked questions about Harness FME", + link: "/docs/faqs/harness-feature-flag-faqs", + }, + { + title: "FME Knowledge base", + module: MODULES.fme, + description: + "In-depth knowledge base articles", + link: "/kb/feature-flags", + }, + ], + }, + */ + ]; + /* Define the cards - end */ \ No newline at end of file diff --git a/src/components/HomepageFeatures/data/featureListData.tsx b/src/components/HomepageFeatures/data/featureListData.tsx index d572cc2e1c9..3494bfda67e 100644 --- a/src/components/HomepageFeatures/data/featureListData.tsx +++ b/src/components/HomepageFeatures/data/featureListData.tsx @@ -53,6 +53,13 @@ export const featureList: CardItem[] = [ description: <>Roll out new features progressively., link: "docs/category/get-started-with-feature-flags", }, + { + title: "Feature Management & Experimentation", + module: MODULES.fme, + icon: "img/icon_fme.svg", + description: <>Switch on data-driven features and releases., + link: "docs/feature-management-experimentation", + }, { title: "Optimize Cloud Costs", module: MODULES.ccm, diff --git a/src/components/HomepageFeatures/styles.module.scss b/src/components/HomepageFeatures/styles.module.scss index 6b2e059e963..af58d4f8e5a 100644 --- a/src/components/HomepageFeatures/styles.module.scss +++ b/src/components/HomepageFeatures/styles.module.scss @@ -74,6 +74,9 @@ &.ff:hover { border-color: var(--mod-ff-200); } + &.fme:hover { + border-color: var(--mod-fme-200); + } &.ssca:hover { border-color: var(--mod-ssca-200); } diff --git a/src/components/Roadmap/HorizonCard/styles.module.scss b/src/components/Roadmap/HorizonCard/styles.module.scss index 0cbba358cd3..991ef2c5b52 100644 --- a/src/components/Roadmap/HorizonCard/styles.module.scss +++ b/src/components/Roadmap/HorizonCard/styles.module.scss @@ -69,6 +69,12 @@ .ff:focus { border-color: var(--mod-ff-200); } +.fme:hover { + border-color: var(--mod-fme-200); +} +.fme:focus { + border-color: var(--mod-fme-200); +} .ccm:hover { border-color: var(--mod-ccm-200); } diff --git a/src/components/Roadmap/data/cdData.ts b/src/components/Roadmap/data/cdData.ts index 2cb17dadd23..9bb7794e307 100644 --- a/src/components/Roadmap/data/cdData.ts +++ b/src/components/Roadmap/data/cdData.ts @@ -336,31 +336,31 @@ export const CdData: Horizon = { tag: [{value: "Deployment"}], title: "Azure Functions", description: "Users can deploy Azure Functions.", - link:"https://developer.harness.io/docs/continuous-delivery/deploy-srv-diff-platforms/azure/azure-function-tutorial/" }, { tag: [{value: "Deployment"}], title: "Google Cloud Run Support", description: "Users can deploy to Google Cloud Run.", - link:"https://developer.harness.io/docs/continuous-delivery/deploy-srv-diff-platforms/google-cloud-functions/google-cloud-run/" + }, + { + tag: [{value: "OPA"}], + title: "Service, Environment, Overrides w/ OPA", + description: "Users can configure Service, Environment, and Overrides with OPA policies.", }, { tag: [{value: "Pipeline"}], title: "Flexible Templates Phase II", description: "Users can reference dynamically inserted stages/steps in pipeline templates", - link:"https://developer.harness.io/docs/platform/templates/inject-step-stage-templates/" }, { tag: [{value: "OPA"}], title: "Service, Environment, Overrides w/ OPA", - description: "Users can create and enforce OPA policies for CD entities such as Services, Environments, Overrides, and Infrastructure definitions", - link:"https://developer.harness.io/docs/continuous-delivery/x-platform-cd-features/advanced/cd-governance/opa-policies-for-cd-entities/" + description: "Users can create and enforce OPA policies for CD entities such as Services, Environments, Overrides, and Infrastructure definitionsn", }, { tag: [{value: "GitOps"}], title: "Improved Application Filtering", description: "Users can filter applications using live search functionality, and wildcard search is also supported for application labels. ", - link:"https://developer.harness.io/docs/continuous-delivery/gitops/use-gitops/manage-gitops-applications/" } ] } diff --git a/src/components/TutorialCard/TutorialCard.module.scss b/src/components/TutorialCard/TutorialCard.module.scss index af7680094c1..3586deda67c 100755 --- a/src/components/TutorialCard/TutorialCard.module.scss +++ b/src/components/TutorialCard/TutorialCard.module.scss @@ -124,6 +124,9 @@ .ff:hover { border-color: var(--mod-ff-200) !important; } +.fme:hover { + border-color: var(--mod-fme-200) !important; +} .ccm:hover { border-color: var(--mod-ccm-200) !important; } diff --git a/src/constants.ts b/src/constants.ts index 7c2366e22a7..cc3f46be7e6 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -53,7 +53,7 @@ export const MODULE_DISPLAY_NAME = { [MODULES.cde]: 'Cloud Development Environments', [MODULES.armory]: 'Armory', [MODULES.opensource]: 'Open Source', - [MODULES.fme]: 'Feature Mgmt & Experimentation' + [MODULES.fme]: 'Feature Management & Experimentation' } export const MODULE_ICON = { diff --git a/src/css/custom.css b/src/css/custom.css index 2432131e1c5..16a317a3c84 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -231,6 +231,15 @@ --mod-ff-200: #ee8625; --mod-ff-100: #fcf4e3; + /* Module Colors/Feature Management & Experimentation/300 */ + --mod-fme-300: #6938c0; + /* Module Colors/Feature Management & Experimentation/300 */ + --mod-fme-200: #8b73ce; + /* Module Colors/Feature Management & Experimentation/300 */ + --mod-fme-100: #f6f1ff; + + --mod-sto-300: #1947ec; + /* Module Colors/STO/200 */ --mod-sto-300: #1947ec; --mod-sto-200: #2660ff; @@ -603,6 +612,10 @@ details summary::before { color: var(--mod-ff-200); } +.color-fme { + color: var(--mod-fme-200); +} + .color-ccm { color: var(--mod-ccm-200); } @@ -1523,6 +1536,9 @@ html[data-theme='dark'] .sidebar-opensource > a::before { .feature-flags:hover { border-color: var(--mod-ff-200) !important; } +.feature-management--experimentation:hover { + border-color: var(--mod-fme-200) !important; +} .cloud-cost-management:hover { border-color: var(--mod-ccm-200) !important; } diff --git a/static/img/fme-docs-main-dark-mode.svg b/static/img/fme-docs-main-dark-mode.svg new file mode 100644 index 00000000000..0b0c1cb7859 --- /dev/null +++ b/static/img/fme-docs-main-dark-mode.svg @@ -0,0 +1,424 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/img/fme-docs-main-light-mode.svg b/static/img/fme-docs-main-light-mode.svg new file mode 100644 index 00000000000..84982e57044 --- /dev/null +++ b/static/img/fme-docs-main-light-mode.svg @@ -0,0 +1,416 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 733012f6b20c4eea4941b458a19dac003883684c Mon Sep 17 00:00:00 2001 From: lena sano Date: Wed, 26 Feb 2025 12:56:33 -0300 Subject: [PATCH 02/19] replace references to Split with Harness or FME --- .../10-getting-started/docs/key-concepts.md | 110 ++++++++++-------- 1 file changed, 61 insertions(+), 49 deletions(-) diff --git a/docs/feature-management-experimentation/10-getting-started/docs/key-concepts.md b/docs/feature-management-experimentation/10-getting-started/docs/key-concepts.md index 34133130962..7d6038a438d 100644 --- a/docs/feature-management-experimentation/10-getting-started/docs/key-concepts.md +++ b/docs/feature-management-experimentation/10-getting-started/docs/key-concepts.md @@ -6,8 +6,12 @@ helpdocs_is_private: false helpdocs_is_published: true --- +

+ +

+ ## Key concepts -Take 5 minutes to learn the foundational concepts of Split’s Feature Data Platform. +Take 5 minutes to learn the foundational concepts of Harness Feature Management & Experimentation. ## What is a feature flag? A feature flag wraps or gates a section of your code, allowing it to be selectively turned on or off remotely with precision, down to the level of an individual user, at any time, without a new code deployment. @@ -18,87 +22,95 @@ Feature flags allow you to decouple your deploy from your release, so your work ### Control your release with targeting rules Once your code is deployed, you can instantly turn on or off features for any individual user, group of users, or percentage of users, by creating or updating targeting rules. This approach facilitates faster software delivery practices with greater safety, including: -Trunk-based development to reduce time lost merging code branches -Testing in production to allow dev, QA, and stakeholder review without impacting your users -Early access or beta testing for a subset of your users in production -Canary releases and monitored rollouts to limit the blast radius of release incidents -Instant kill switches to shut off exposure to a feature without rollback or redeploy -Infrastructure migration without downtime or risk of data loss -Experimentation and A/B testing to make bigger bets with less risk +* Trunk-based development to reduce time lost merging code branches +* Testing in production to allow dev, QA, and stakeholder review without impacting your users +* Early access or beta testing for a subset of your users in production +* Canary releases and monitored rollouts to limit the blast radius of release incidents +* Instant kill switches to shut off exposure to a feature without rollback or redeploy +* Infrastructure migration without downtime or risk of data loss +* Experimentation and A/B testing to make bigger bets with less risk -## The role of data in Split -The Split Feature Data Platform provides visibility into your controlled releases by comparing data about feature flag evaluations with data about what happened after those evaluations. The data points that feed those comparisons are impressions and events. The results of those comparisons are called metrics. +## The role of data in Harness FME +The FME provides visibility into your controlled releases by comparing data about feature flag evaluations with data about what happened after those evaluations. The data points that feed those comparisons are impressions and events. The results of those comparisons are called metrics. ### Impressions -An impression is a record of a targeting decision made. It is created automatically each time a feature flag is evaluated and contains details about the user or unique key for which the evaluation was performed, the targeting decision, the targeting rule that drove that decision, and a time stamp. Refer to the Impressions guide for more information. +An impression is a record of a targeting decision made. It is created automatically each time a feature flag is evaluated and contains details about the user or unique key for which the evaluation was performed, the targeting decision, the targeting rule that drove that decision, and a time stamp. Refer to the [Impressions](https://help.split.io/hc/en-us/articles/360020585192-Impressions) guide for more information. ### Events -An event is a record of user or system behavior. Events can be as simple as a page visited, a button clicked, or response time observed, and as complex as a transaction record with a detailed list of properties. An event doesn’t refer to a feature flag. The association between flag evaluations and events is computed for you. An event, associated with a user (or other unique keys), arriving after a flag decision for that same unique key, is attributed to that evaluation by Split’s attribution engine. +An event is a record of user or system behavior. Events can be as simple as a page visited, a button clicked, or response time observed, and as complex as a transaction record with a detailed list of properties. An event doesn’t refer to a feature flag. The association between flag evaluations and events is computed for you. An event, associated with a user (or other unique keys), arriving after a flag decision for that same unique key, is attributed to that evaluation by FME’s attribution engine. -To be ingested by Split, an event must contain the same user or unique key for which a feature flag evaluation was performed and a time stamp. Events are sent to Split from within your application, either from an existing customer data platform or error subsystem, or with a bulk upload using Split’s REST API. Numerous events in integrations streamline event ingest for you. +To be ingested by FME, an event must contain the same user or unique key for which a feature flag evaluation was performed and a time stamp. Events are sent to FME from within your application, either from an existing customer data platform or error subsystem, or with a bulk upload using [Split Admin API](https://docs.split.io). Numerous events in integrations streamline event ingest for you. ### Metrics -Split calculates metrics by attributing events to impressions and applying metric definitions to them. A metric definition can be as simple as a count of events per user or as complex as an average of values pulled from an event’s property after filtering those same events by another property. +FME calculates metrics by attributing events to impressions and applying metric definitions to them. A metric definition can be as simple as a count of events per user or as complex as an average of values pulled from an event’s property after filtering those same events by another property. For example, from a stream of room_reservation events, calculate the average number of room nights booked for platinum members by examining the room_nights property after filtering the room_reservation events to those where the property club_membership = platinum. -To promote one version of the truth, metrics are defined in a central location, not on a flag-by-flag basis, and all metrics are calculated for all flags. Split lets you elevate any metric your account created to be a key metric for a given feature flag. Then all the remaining metrics are sorted by impact and displayed immediately below the key metrics. This design, unique to Split, avoids blind spots caused by only looking for what you expect to find which automatically surfaces unexpected impacts. Refer to the Metrics guide for more information. +To promote one version of the truth, metrics are defined in a central location, not on a flag-by-flag basis, and all metrics are calculated for all flags. FME lets you elevate any metric your account created to be a key metric for a given feature flag. Then all the remaining metrics are sorted by impact and displayed immediately below the key metrics. This design, unique to FME, avoids blind spots caused by only looking for what you expect to find which automatically surfaces unexpected impacts. Refer to the [Metrics](https://help.split.io/hc/en-us/articles/22005565241101-Metrics) guide for more information. ### Alerts Alerts notify metric stakeholders and the team rolling out a particular feature when a metric threshold has exceeded a rollout or experiment that uses a percentage rollout rule. -Alerts, like the metrics they are based on, are centrally defined once, and then applied to every rollout or experiment automatically. This is another design unique to Split. Our goal is to make learning and safety at speed the default experience, for every rollout. Once you define thresholds for metrics, any future rollout or experiment that exceeds them will fire an alert. When that happens, notifications are sent out, and an alert box is presented on the Targeting and Alerts tabs for the feature flag in question. Refer to the Configuring metric alerting guide for more information. +Alerts, like the metrics they are based on, are centrally defined once, and then applied to every rollout or experiment automatically. This is another design unique to FME. Our goal is to make learning and safety at speed the default experience, for every rollout. Once you define thresholds for metrics, any future rollout or experiment that exceeds them will fire an alert. When that happens, notifications are sent out, and an alert box is presented on the Targeting and Alerts tabs for the feature flag in question. Refer to the [Configuring metric alerting](https://help.split.io/hc/en-us/articles/19832312225293-Configuring-metric-alerting) guide for more information. -Using Split in your application -Targeting decisions are made locally, in memory, from within your own application code. There is never a reason to send private user data to Split’s network. Let’s take a look at how this is accomplished. +## Using FME in your application +Targeting decisions are made locally, in memory, from within your own application code. There is never a reason to send private user data to FME's network. Let’s take a look at how this is accomplished. -Split SDKs -To use Split, include and initialize one of Split’s SDKs in your application. Once the SDK is initialized, targeting rules are retrieved from a nearby content delivery network (CDN) node, cached inside your code, and updated in real-time in milliseconds using a streaming architecture. +### FME SDKs +To use Harness FME, include and initialize one of FME SDKs in your application. Once the SDK is initialized, targeting rules are retrieved from a nearby content delivery network (CDN) node, cached inside your code, and updated in real-time in milliseconds using a streaming architecture. -As needed, your application makes a just-in-time call to the Split SDK in local memory, passing the feature flag name, the userId or unique key, and optionally, a map of user or session attributes. The response is returned instantly, with no need for a network call. After the evaluation is performed, the SDK asynchronously returns an impression record to Split. Refer to our SDK overview for more information. +As needed, your application makes a just-in-time call to the FME SDK in local memory, passing the feature flag name, the userId or unique key, and optionally, a map of user or session attributes. The response is returned instantly, with no need for a network call. After the evaluation is performed, the SDK asynchronously returns an impression record to Harness. Refer to our [SDK overview](https://help.split.io/hc/en-us/articles/360033557092-SDK-overview) for more information. -Split evaluator -As an alternative to using Split’s SDKs, you can make REST API calls to a Split Evaluator hosted inside your own infrastructure. Like the SDK, this method never requires you to send private user data to Split’s network. The evaluator makes it possible to operate from within languages that do not yet have a published Split SDK and should only be used in that case. Refer to the Split evaluator guide for more information. +### Split evaluator +As an alternative to using FME SDKs, you can make REST API calls to a Split Evaluator hosted inside your own infrastructure. Like the SDK, this method never requires you to send private user data to the Harness network. The evaluator makes it possible to operate from within languages that do not yet have a published FME SDK and should only be used in that case. Refer to the [Split evaluator](https://help.split.io/hc/en-us/articles/360020037072-Split-Evaluator) guide for more information. -Split's structure -Split is architected to support teams and organizations of any size, from a single developer to multiple value-stream enterprises. Take a moment to familiarize yourself with the concepts of your Split account, project, environment, and objects, e.g., users, groups, tags, traffic types, feature flags, segments, and metrics. +## FME's structure +Harness FME is architected to support teams and organizations of any size, from a single developer to multiple value-stream enterprises. Take a moment to familiarize yourself with the concepts of your Harness account, project, environment, and objects, e.g., users, groups, tags, traffic types, feature flags, segments, and metrics. ![](https://help.split.io/hc/article_attachments/30794709286029) -Account -Your company has one Split account. Your account is the highest level container. Split support may ask you for your account ID to speed troubleshooting. You’ll find your account ID in the URL for every page you visit in the Split application. +### Account +Your company has one Harness account. Your account is the highest level container. Harness FME support may ask you for your account ID to speed troubleshooting. You’ll find your account ID in the URL for every page you visit in the FME application. + +### Users +A Harness user is someone with access to the Harness user interface. Administrators can invite new users to Harness. All paid plans include SSO for user authentication and can support either invites or just in time provisioning. -Users -A Split user is someone with access to the Split user interface. Administrators can invite new users to Split. All paid plans include SSO for user authentication and can support either invites or just in time provisioning. +### Groups +A group is a convenient way to manage a collection of users in your account. You can use groups to grant administrative controls and grant environment, feature flag, or segment-level controls. Refer to the [Manage user groups](https://help.split.io/hc/en-us/articles/360020812952-Manage-user-groups) guide for more information. -Groups -A group is a convenient way to manage a collection of users in your account. You can use groups to grant administrative controls and grant environment, feature flag, or segment-level controls. Refer to the Manage user groups guide for more information. +### Projects +Projects provide separation or partitioning of work to reduce clutter or to enforce security. All accounts have at least one project. Use multiple projects only when you want to deliberately separate the work of different teams, product lines, or areas of work from each other. By design, objects within FME are not meant to be shared or moved across projects. Refer to the [Projects](https://help.split.io/hc/en-us/articles/360023534451-Workspaces) guide for more information. -Projects -Projects provide separation or partitioning of work to reduce clutter or to enforce security. All accounts have at least one project. Use multiple projects only when you want to deliberately separate the work of different teams, product lines, or areas of work from each other. By design, objects within Split are not meant to be shared or moved across projects. Refer to the Projects guide for more information. +### Environment +Within each project, you may have multiple environments, such as development, staging, and production. Refer to the [Environments](https://help.split.io/hc/en-us/articles/360019915771-Environments) guide for more information. -Environment -Within each project, you may have multiple environments, such as development, staging, and production. Refer to the Environments guide for more information. +### Feature Flags +Feature flags are created at the project level where you specify the feature flag name, traffic type, owners, and description. Targeting rules are then created and managed at the environment level as part of the feature flag definition. Refer to the [Feature flag management](https://help.split.io/hc/en-us/articles/9650375859597-Feature-flag-management) guide for more information. -Feature Flags -Feature flags are created at the project level where you specify the feature flag name, traffic type, owners, and description. Targeting rules are then created and managed at the environment level as part of the feature flag definition. Refer to the Feature flag management guide for more information. +### Targeting rule +Targeting rules for each feature flag are created at the environment level. For example, this supports one set of rules in your staging environment and another in production. Rules may be based on user or device attributes, membership in a segment, a percentage of a randomly distributed population, a list of individually specified user or unique key targets, or any combination of the above. -Targeting rule -Targeting rules for each feature flag are created at the environment level. For example, this supports one set of rules in your staging environment and another in production. Rules may be based on user or device attributes, membership in a segment, a percentage of a randomly distributed population, a list of individually specified user or unique key targets, or any combination of the above. Refer to the Creating a rollout plan guide for more information. + -Segment -A segment is a list of users or unique keys for targeting purposes. Segments are created at the environment level. Refer to the Segments guide for more information. +### Segment +A segment is a list of users or unique keys for targeting purposes. Segments are created at the environment level. Refer to the [Segments](https://help.split.io/hc/en-us/articles/360020407512-Create-a-segment) guide for more information. -Traffic type +### Traffic type Targeting decisions are made on a per-user or per unique key basis, but what are the available types of unique keys you intend to target? These are your traffic types, and you can define up to ten unique key types at the project level. -For feature flags that make decisions or observe metrics at the userId level, the traffic type should be user. If decisions and observations are based on account membership (to facilitate all users for a particular customer being treated the same, for instance), the traffic type should be account. Other common types are anonymous and device, but you have total flexibility in employing different traffic types. Refer to the Traffic type guide for more information. +For feature flags that make decisions or observe metrics at the userId level, the traffic type should be user. If decisions and observations are based on account membership (to facilitate all users for a particular customer being treated the same, for instance), the traffic type should be account. Other common types are anonymous and device, but you have total flexibility in employing different traffic types. Refer to the [Traffic type](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) guide for more information. + +### Tag +Use tags to organize and filter feature flags, segments, and metrics across the Harness user interface. Because they allow you to filter items in lists, they are a great way to filter by team, epic, layer of system (front-end vs back-end), or any other. Refer to the [Tags](https://help.split.io/hc/en-us/articles/360020839151-Tags) guide for more information on how to use them. -Tag -Use tags to organize and filter feature flags, segments, and metrics across the Split user interface. Because they allow you to filter items in lists, they are a great way to filter by team, epic, layer of system (front-end vs back-end), or any other. Refer to the Tags guide for more information on how to use them. +### Statuses +Statuses provide a way for teams to indicate which stage of a release or rollout a feature is in at any given moment, and as a way for teammates to filter their feature flags to see only features in a particular stage of the internal release process. There is a fixed list of status types. Refer to the [Use statuses](https://help.split.io/hc/en-us/articles/4405023981197-Use-statuses) guide for more information. -Statuses -Statuses provide a way for teams to indicate which stage of a release or rollout a feature is in at any given moment, and as a way for teammates to filter their feature flags to see only features in a particular stage of the internal release process. There is a fixed list of status types. Refer to the Use statuses guide for more information. + \ No newline at end of file From 40c18257a7445b14300b07f06d7257bd3b902d8e Mon Sep 17 00:00:00 2001 From: lena sano Date: Wed, 26 Feb 2025 13:07:01 -0300 Subject: [PATCH 03/19] replace references to Split with Harness or FME (proof reading edits) --- .../10-getting-started/docs/key-concepts.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/feature-management-experimentation/10-getting-started/docs/key-concepts.md b/docs/feature-management-experimentation/10-getting-started/docs/key-concepts.md index 7d6038a438d..09c0c6ddd61 100644 --- a/docs/feature-management-experimentation/10-getting-started/docs/key-concepts.md +++ b/docs/feature-management-experimentation/10-getting-started/docs/key-concepts.md @@ -31,7 +31,7 @@ Once your code is deployed, you can instantly turn on or off features for any in * Experimentation and A/B testing to make bigger bets with less risk ## The role of data in Harness FME -The FME provides visibility into your controlled releases by comparing data about feature flag evaluations with data about what happened after those evaluations. The data points that feed those comparisons are impressions and events. The results of those comparisons are called metrics. +FME provides visibility into your controlled releases by comparing data about feature flag evaluations with data about what happened after those evaluations. The data points that feed those comparisons are impressions and events. The results of those comparisons are called metrics. ### Impressions An impression is a record of a targeting decision made. It is created automatically each time a feature flag is evaluated and contains details about the user or unique key for which the evaluation was performed, the targeting decision, the targeting rule that drove that decision, and a time stamp. Refer to the [Impressions](https://help.split.io/hc/en-us/articles/360020585192-Impressions) guide for more information. @@ -54,7 +54,7 @@ Alerts notify metric stakeholders and the team rolling out a particular feature Alerts, like the metrics they are based on, are centrally defined once, and then applied to every rollout or experiment automatically. This is another design unique to FME. Our goal is to make learning and safety at speed the default experience, for every rollout. Once you define thresholds for metrics, any future rollout or experiment that exceeds them will fire an alert. When that happens, notifications are sent out, and an alert box is presented on the Targeting and Alerts tabs for the feature flag in question. Refer to the [Configuring metric alerting](https://help.split.io/hc/en-us/articles/19832312225293-Configuring-metric-alerting) guide for more information. ## Using FME in your application -Targeting decisions are made locally, in memory, from within your own application code. There is never a reason to send private user data to FME's network. Let’s take a look at how this is accomplished. +Targeting decisions are made locally, in memory, from within your own application code. There is never a reason to send private user data to Harness. Let’s take a look at how this is accomplished. ### FME SDKs To use Harness FME, include and initialize one of FME SDKs in your application. Once the SDK is initialized, targeting rules are retrieved from a nearby content delivery network (CDN) node, cached inside your code, and updated in real-time in milliseconds using a streaming architecture. From f78a2b6a97bde5e5915e5146140d93ab3b0784c2 Mon Sep 17 00:00:00 2001 From: lena sano Date: Thu, 27 Feb 2025 12:17:34 -0300 Subject: [PATCH 04/19] maintain infra names (Split Synchronizer/Evaluator/Proxy), proof reading edits --- .../10-getting-started/docs/key-concepts.md | 2 +- .../10-getting-started/docs/overview.md | 2 +- .../10-getting-started/whats-supported.md | 21 ++++++++++--------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/docs/feature-management-experimentation/10-getting-started/docs/key-concepts.md b/docs/feature-management-experimentation/10-getting-started/docs/key-concepts.md index 09c0c6ddd61..d7cb7bca822 100644 --- a/docs/feature-management-experimentation/10-getting-started/docs/key-concepts.md +++ b/docs/feature-management-experimentation/10-getting-started/docs/key-concepts.md @@ -70,7 +70,7 @@ Harness FME is architected to support teams and organizations of any size, from ![](https://help.split.io/hc/article_attachments/30794709286029) ### Account -Your company has one Harness account. Your account is the highest level container. Harness FME support may ask you for your account ID to speed troubleshooting. You’ll find your account ID in the URL for every page you visit in the FME application. +Your company has one Harness account. Your account is the highest level container. Harness FME support may ask you for your account ID to speed troubleshooting. You’ll find your account ID in the URL for every page you visit in Harness. ### Users A Harness user is someone with access to the Harness user interface. Administrators can invite new users to Harness. All paid plans include SSO for user authentication and can support either invites or just in time provisioning. diff --git a/docs/feature-management-experimentation/10-getting-started/docs/overview.md b/docs/feature-management-experimentation/10-getting-started/docs/overview.md index 410b5e404d7..7e3647e1232 100644 --- a/docs/feature-management-experimentation/10-getting-started/docs/overview.md +++ b/docs/feature-management-experimentation/10-getting-started/docs/overview.md @@ -11,7 +11,7 @@ helpdocs_is_published: true

-Harness Feature Management & Experimentation (FME) combines capabilities for feature delivery and control with built-in tools for measurement and learning. FME connects insightful data to every feature release, eliminates hesitation from the software development process, and supports modern practices like continuous delivery and progressive delivery. +Harness Feature Management & Experimentation (FME) combines capabilities for feature delivery and control with built-in tools for measurement and learning. FME connects insightful data to every feature release and supports modern practices like continuous delivery and progressive delivery. ![](./static/overview.png) diff --git a/docs/feature-management-experimentation/10-getting-started/whats-supported.md b/docs/feature-management-experimentation/10-getting-started/whats-supported.md index a4dd4e0d273..bf1bd04b648 100644 --- a/docs/feature-management-experimentation/10-getting-started/whats-supported.md +++ b/docs/feature-management-experimentation/10-getting-started/whats-supported.md @@ -21,7 +21,8 @@ The following table lists the server-side FME SDKs that Harness supports. | SDK | Documentation | | ---- | --- | | [Go](https://github.com/splitio/go-client) | [Go SDK reference](https://help.split.io/hc/en-us/articles/360020093652-Go-SDK) | -| [Java](https://github.com/splitio/java-client) | [Java SDK reference](https://help.split.io/hc/en-us/articles/360020405151-Java-SDK) | +| [Elixir Thin Client](https://github.com/splitio/elixir-thin-client) | [Elixir Thin Client SDK reference](https://help.split.io/hc/en-us/articles/26988707417869-Elixir-Thin-Client-SDK) | +| [Java](https://github.com/splitio/java-client) | [Java SDK reference](https://help.split.io/hc/en-us/articles/360020405151-Java-SDK) | | [.NET](https://github.com/splitio/dotnet-client) | [.NET SDK Reference](https://help.split.io/hc/en-us/articles/360020240172--NET-SDK) | | [Node.js](https://github.com/splitio/javascript-client) | [Node.js SDK Reference](https://help.split.io/hc/en-us/articles/360020564931-Node-js-SDK) | | [PHP](https://github.com/splitio/php-client) | [PHP SDK reference](https://help.split.io/hc/en-us/articles/360020350372-PHP-SDK) | @@ -47,7 +48,7 @@ The following table lists the client-side FME SDKs that Harness supports. ## Supported RUM Agents and Suite SDKs -RUM Agents collect Real User Metric events and sends these events to Harness. Harness FME also supports FME Suite SDKs that include RUM Agents. The following table lists the FME RUM Agents and FME Suite SDKs that Harness supports. +RUM Agents collect Real User Metric events and send these events to Harness. Harness FME also supports FME Suite SDKs that include RUM Agents. The following table lists the FME RUM Agents and FME Suite SDKs that Harness supports. | FME Suite SDK | FME Suite SDK documentation | RUM Agent documentation | | ---- | --- | --- | @@ -55,25 +56,25 @@ RUM Agents collect Real User Metric events and sends these events to Harness. Ha | [iOS](https://github.com/splitio/ios-client) | [iOS Suite SDK reference](https://help.split.io/hc/en-us/articles/26408115004429-iOS-Suite) | [iOS RUM Agent reference](https://help.split.io/hc/en-us/articles/22545155055373-iOS-RUM-Agent) | | [JavaScript Browser](https://github.com/splitio/javascript-browser-client) | [JavaScript Browser Suite SDK Reference](https://help.split.io/hc/en-us/articles/22622277712781-Browser-Suite) | [JavaScript Browser RUM Agent reference](https://help.split.io/hc/en-us/articles/360030898431-Browser-RUM-Agent) | -## FME Evaluator +## Split Evaluator -For languages where there is no native SDK support, Harness offers the [FME Evaluator](https://help.split.io/hc/en-us/articles/360020037072-Split-Evaluator), a small service capable of evaluating some or all available features for a given customer via a REST endpoint. +For languages where there is no native SDK support, Harness offers the [Split Evaluator](https://help.split.io/hc/en-us/articles/360020037072-Split-Evaluator), a small service capable of evaluating some or all available FME features for a given customer via a REST endpoint. -## FME Synchronizer +## Split Synchronizer -The [FME Synchronizer](https://help.split.io/hc/en-us/articles/360019686092-Split-Synchronizer) service is built for languages that do not have a native capability to keep a shared local cache, which is needed to evaluate FME feature flags. +The [Split Synchronizer](https://help.split.io/hc/en-us/articles/360019686092-Split-Synchronizer) service is built for languages that do not have a native capability to keep a shared local cache, which is needed to evaluate FME feature flags. -This tool coordinates the sending and receiving of data to a remote datastore that all of your processes can share. Out of the box, FME Synchronizer supports Redis as a remote datastore. The Synchronizer service runs as a standalone process in dedicated or shared servers and it does not affect the performance of your code or FME SDKs. +This tool coordinates the sending and receiving of data to a remote datastore that all of your processes can share. Out of the box, Split Synchronizer supports Redis as a remote datastore. The Synchronizer service runs as a standalone process in dedicated or shared servers and it does not affect the performance of your code or FME SDKs. -## FME Proxy +## Split Proxy -The [FME Proxy](https://help.split.io/hc/en-us/articles/4415960499213-Split-Proxy) enables you to deploy a service in your own infrastructure that behaves like Harness servers and is used by both server-side and client-side SDKs to synchronize the flags without connecting to Harness FME's actual backend directly. +The [Split Proxy](https://help.split.io/hc/en-us/articles/4415960499213-Split-Proxy) enables you to deploy a service in your own infrastructure that behaves like Harness servers and is used by both server-side and client-side SDKs to synchronize the flags without connecting to Harness FME's actual backend directly. This tool reduces connection latencies from the SDKs to the Harness servers transparently, and when a single connection is required from a private network to the outside for security reasons. ## Running in the Cloud -There are no limitations for using FME in any cloud or non-cloud environment as long as the languages needed are supported with an SDK, and connectivity to either Harness or the FME Proxy can be established. +There are no limitations for using FME in any cloud or non-cloud environment as long as the languages needed are supported with an SDK, and connectivity to either Harness or the Split Proxy can be established. For information about what's supported for other Harness modules and the Harness Platform overall, go to [Supported platforms and technologies](/docs/platform/platform-whats-supported.md). From 73dfe334747035bf11704474832b8df99363eafd Mon Sep 17 00:00:00 2001 From: lena sano Date: Mon, 3 Mar 2025 12:45:09 -0300 Subject: [PATCH 05/19] FME SDK and infra docs - copy and paste from github.com/splitio --- .../10-getting-started/whats-supported.md | 3 +- .../_category_.json | 6 + .../best-practices/_category_.json | 7 + .../best-practices/split-sync-runbook.md | 384 ++++ .../client-side-agents/_category_.json | 7 + .../client-side-agents/android-rum-agent.md | 381 ++++ .../client-side-agents/browser-rum-agent.md | 494 ++++++ .../client-side-agents/ios-rum-agent.md | 259 +++ .../client-side-sdk-examples/_category_.json | 7 + .../client-side-sdks/_category_.json | 7 + .../client-side-sdks/android-sdk.md | 1492 ++++++++++++++++ .../client-side-sdks/angular-utilities.md | 569 ++++++ .../client-side-sdks/browser-sdk.md | 1357 +++++++++++++++ .../client-side-sdks/flutter-plugin.md | 732 ++++++++ .../client-side-sdks/ios-sdk.md | 894 ++++++++++ .../client-side-sdks/javascript-sdk.md | 1240 +++++++++++++ .../client-side-sdks/react-native-sdk.md | 1249 +++++++++++++ .../client-side-sdks/react-sdk.md | 1426 +++++++++++++++ .../client-side-sdks/redux-sdk.md | 1166 +++++++++++++ .../client-side-suites/_category_.json | 7 + .../client-side-suites/android-suite.md | 1388 +++++++++++++++ .../client-side-suites/browser-suite.md | 1255 +++++++++++++ .../client-side-suites/ios-suite.md | 853 +++++++++ .../faqs-client-side-sdks/_category_.json | 7 + .../faqs-general-sdk/_category_.json | 7 + .../faqs-optional-infra/_category_.json | 7 + .../faqs-server-side-sdks/_category_.json | 7 + .../optional-infra/_category_.json | 7 + .../optional-infra/split-daemon-splitd.md | 378 ++++ .../optional-infra/split-evaluator.md | 749 ++++++++ .../split-javascript-synchronizer-tools.md | 179 ++ .../optional-infra/split-proxy.md | 550 ++++++ .../optional-infra/split-synchronizer.md | 921 ++++++++++ .../sdk-overview/sdk-overview.md | 145 ++ .../sdk-overview/sdk-validation-checklist.md | 93 + .../sdk-overview/sdk-versioning-policy.md | 43 + .../sdk-overview/troubleshooting.md | 145 ++ .../server-side-sdk-examples/_category_.json | 7 + .../server-side-sdks/_category_.json | 7 + .../elixir-thin-client-sdk.md | 321 ++++ .../server-side-sdks/go-sdk.md | 914 ++++++++++ .../server-side-sdks/java-sdk.md | 1535 ++++++++++++++++ .../server-side-sdks/net-sdk.md | 1029 +++++++++++ .../server-side-sdks/nodejs-sdk.md | 1153 ++++++++++++ .../server-side-sdks/php-sdk.md | 672 +++++++ .../server-side-sdks/php-thin-client-sdk.md | 442 +++++ .../server-side-sdks/python-sdk.md | 1546 +++++++++++++++++ .../server-side-sdks/ruby-sdk.md | 595 +++++++ sidebars.ts | 7 + 49 files changed, 26648 insertions(+), 1 deletion(-) create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/_category_.json create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/_category_.json create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/split-sync-runbook.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-agents/_category_.json create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-agents/android-rum-agent.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-agents/browser-rum-agent.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-agents/ios-rum-agent.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/_category_.json create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/_category_.json create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/android-sdk.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/angular-utilities.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/browser-sdk.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/flutter-plugin.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/ios-sdk.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/javascript-sdk.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/react-native-sdk.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/react-sdk.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/redux-sdk.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/_category_.json create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/android-suite.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/browser-suite.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/ios-suite.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/_category_.json create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/_category_.json create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/_category_.json create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/_category_.json create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/_category_.json create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-daemon-splitd.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-evaluator.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-javascript-synchronizer-tools.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-proxy.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-synchronizer.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/sdk-overview.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/sdk-validation-checklist.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/sdk-versioning-policy.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/troubleshooting.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/_category_.json create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/_category_.json create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/elixir-thin-client-sdk.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/go-sdk.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/java-sdk.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/net-sdk.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/nodejs-sdk.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/php-sdk.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/php-thin-client-sdk.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/python-sdk.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/ruby-sdk.md diff --git a/docs/feature-management-experimentation/10-getting-started/whats-supported.md b/docs/feature-management-experimentation/10-getting-started/whats-supported.md index bf1bd04b648..347dcc72a5f 100644 --- a/docs/feature-management-experimentation/10-getting-started/whats-supported.md +++ b/docs/feature-management-experimentation/10-getting-started/whats-supported.md @@ -6,6 +6,7 @@ sidebar_position: 1 helpdocs_is_private: false helpdocs_is_published: true --- + This topic lists platform and technologies supported by Harness Feature Management & Experimentation (FME). For more information about FME features and functionality, go to the [Harness FME overview](/docs/feature-management-experimentation/10-getting-started/docs/overview.md). @@ -152,4 +153,4 @@ Some of our best ideas come from our customers. You can submit your feature requ ## What else does Harness support? -For information about what's supported for other Harness modules and the Harness Software Delivery Platform overall, go to [Supported platforms and technologies](/docs/platform/platform-whats-supported.md). +For information about what's supported for other Harness modules and the Harness Software Delivery Platform overall, go to [Supported platforms and technologies](/docs/platform/platform-whats-supported.md). \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/_category_.json b/docs/feature-management-experimentation/20-sdks-and-infrastructure/_category_.json new file mode 100644 index 00000000000..263af214c13 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "SDKs & Infrastructure", + "collapsible": "true", + "collapsed": "true", + "className": "red" +} \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/_category_.json b/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/_category_.json new file mode 100644 index 00000000000..6d118df9e5f --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Best practices", + "collapsible": "true", + "collapsed": "true", + "className": "red", + "position": 9 +} \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/split-sync-runbook.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/split-sync-runbook.md new file mode 100644 index 00000000000..8eb1f466ae5 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/split-sync-runbook.md @@ -0,0 +1,384 @@ +--- +title: Split Synchronizer runbook +sidebar_label: Split Synchronizer runbook +helpdocs_is_private: false +helpdocs_is_published: true +--- + + +

+ +

+ +This article includes best practices for running a Split Synchronizer v5.0.0. By default, Split’s SDKs keep segment and feature flag data synchronized as users navigate across disparate systems, treatments and conditions. However, some languages don’t have a native capability to keep a shared local cache of this data to properly serve treatments. + +The Split Synchronizer coordinates the sending and receiving of data to a remote datastore (Redis) that all of your processes can share to pull data for the evaluation of treatments, acting as the cache for your SDKs. It also posts impression data and metrics generated by the SDKs back to Split’s servers, for exposure in the Split user interface or sending to the data integration of your choice. + + + +For more information on configuring the Synchronizer, refer to the [Split synchronizer guide](https://help.split.io/hc/en-us/articles/360019686092-Split-synchronizer-proxy). + +The synchronizer runs as a standalone process in dedicated or shared servers, which doesn’t affect the performance of your code or Split’s SDKs. The following are best practices as it relates to running the Synchronizer. + +## SDKs + +**Alerting on CONTROL treatment** In spite of CONTROL being a known treatment, when an application starts to suddenly report the CONTROL treatment, it's a sign of something wrong when evaluating splits. We recommend setting up an impressions listener that uses StatsD metrics for the CONTROL treatment. For information about impressions listener for Split Sync refer to the [Split Synchronizer](https://help.split.io/hc/en-us/articles/360019686092-Split-Synchronizer#listener) guide. + +**Logs.** We recommend reporting and alerting on errors coming from Split logs. Anything labeled as error or exception from the Split logs should be of concern. One way to isolate Split logs is to direct them to a custom location following the logging instructions for each SDK. + +## Split Sync + +Split Sync is written in GoLang and is highly performant compared with JVM-based and interpreted languages. The following are relevant topics of interest when running Split Sync for a production workload. + +### Setup + +We recommend running Split Sync under supervision, to make sure the process can be brought back up in the event of a crash. + +**Redis:** Set up Split Sync in its own Redis database. We DO NOT recommend using the database zero (default) as it is usually used by other applications. + +**Resiliency with Redis:** As Redis and the Split Synchronizer add additional services to your infrastructure, be sure to properly configure for situations where Redis may be unavailable. The individual SDKs have their own Redis timeout configuration, which needs to be configured to a number appropriate to your infrastructure. If the SDK times out, the SDK returns the control treatment. Code that uses the SDK must ensure that it handles the control treatment appropriately. + +### Hardware requirements + +Split has done extensive testing, but it is important to understand that each environment is different. The minimum requirements we tested using Amazon AWS cloud for a production workload were: + +- **Split Sync process:** AWS EC2 m5.large/m5a.large, 2 vCPUs, 8GB RAM + +- **Redis:** AWS ElastiCache cache.m5.large, 2 vCPUs, 6GB RAM + +### Benchmark + +We have run some tests for the scenario with about 100k impressions per synchronizer + Redis node pairs, on an 8 node Redis cluster. + +We tested the setup with a few scenarios, using the machines that are outline below and all metrics were healthy: + +Redis machines: cache.m5.4xlarge from AWS + +- 16 cores and 52gb ram, up to 10 Gigabit network + +- We used 8 masters with a replica for each of them. + +Split Synchronizer machines running inside a cluster in k8s, on top of a c5a.24xlarge instance type (from AWS, 96 cores + 192GB Ram) + +- Each synchronizer instance was assigned: + + - 8 cores + + - 32 gb ram + +We validated a few scenarios and no performance problems were observed. +The scenarios are the following: + +- Slow ramp up of traffic from 200k to 800k impressions per second + +- Full ramp to 800k impressions per second + +- Simulated a spike in traffic of 2X to 1.6M impressions per second + +- Stopped a Synchronizer for 5m, then restarted it. + +### Alerts + +We recommend the following alerts: + +- **Split Sync process.** Keep CPU under 50% utilization to avoid any performance degradation and to prevent the Split Sync process from falling behind. + +- **Redis.** Keep CPU under 50% utilization. Memory should remain under safe limits, 60 or 70% should be ok, but make sure to monitor the rate of growth. Running at all times at 70% constant utilization could be ok, however if the rate of growth is 10% every 5 minutes that will likely be a problem, and a sign that Split Sync is not able to keep up with evicting data. If Redis memory continues to increase, try the following procedure: + + - Stop Split Sync + [gracefully](https://help.split.io/hc/en-us/articles/360019686092-Split-synchronizer-proxy#service-shutdown) + to avoid losing data. + + - Increase by two (2) the number of threads dedicated to post impressions. Config key: impressionsThreads. + + - Start Split Sync again. + + - Repeat if the memory consumption remains in an increasing pattern. + +Alerting on CONTROL treatment can also be set at the Split Synchronizer level by setting an impression listener described in the [Split Synchronizer guide.](https://help.split.io/hc/en-us/articles/360019686092-Split-synchronizer-proxy#listener) This approach is similar to the SDK as described at the top of this runbook, but from the Synchronizer standpoint. + +### Health Check Monitors + +We have two monitors to periodically validate the Synchronizer health. + +One monitor is in charge of the health of the application which means it verifies that the Synchronizer synchronization tasks are running correctly and it has access to the storage. + +- In order to consume this information, issue a GET request to /health/application. This endpoint has two possible responses: + +- 200 OK +``` +{ + "healthy": true, + "healthySince": "2021-10-29T15:59:21.231209-03:00", + "items": [ + { + "name": "Splits", + "healthy": true, + "lastHit": "2021-10-29T16:01:52.04807-03:00" + }, + { + "name": "Segments", + "healthy": true, + "lastHit": "2021-10-29T16:01:52.106651-03:00" + }, + { + "name": "Storage", + "healthy": true, + "lastHit": "2021-10-29T16:19:21.446657-03:00" + } + ] +} +``` + +- 500 Internal Server Error +``` +{ + "healthy": false, + "items": [ + { + "name": "Splits", + "healthy": false, + "lastHit": "2021-10-29T16:01:52.04807-03:00" + }, + { + "name": "Segments", + "healthy": true, + "lastHit": "2021-10-29T16:01:52.106651-03:00" + }, + { + "name": "Storage", + "healthy": true, + "lastHit": "2021-10-29T16:19:21.446657-03:00" + } + ] +} +``` + +If the monitor detects that the Synchronizer isn’t syncing in a threshold of time, this fails and returns 500. The Synchronizer calculates the threshold from the refresh rate or from the expiration token if it is running in streaming mode. + +The second monitor is in charge of the health of the dependencies, it verifies the health of the external services that the Synchronizer consumes. + +- In order to consume this information, you should issue a GET request + to /health/dependencies + +This endpoint always returns 200 along with the state for each dependency. + +``` +{ + "serviceStatus": "healthy", + "dependencies": [ + { + "service": "https://telemetry.split.io/health", + "healthy": true, + "healthySince": "2021-10-29T16:34:58.272479-03:00" + }, + { + "service": "https://auth.split.io/health", + "healthy": true, + "healthySince": "2021-10-29T16:34:58.272484-03:00" + }, + { + "service": "https://sdk.split.io/api/health", + "healthy": true, + "healthySince": "2021-10-29T16:34:58.272486-03:00" + }, + { + "service": "https://events.split.io/api/health", + "healthy": true, + "healthySince": "2021-10-29T16:34:58.272487-03:00" + }, + { + "service": "https://streaming.split.io/health", + "healthy": true, + "healthySince": "2021-10-29T16:34:58.272488-03:00" + } + ] +} +``` + +### Alerting on logs + +To augment alerts using log-based alerts, considering the following: + +During Redis errors, the Split Sync shows the following lines: + +`connect: connection refused` + +For any other I/O errors, you should see: + +``` +Error fetching segment +Error fetching splits +``` +or, for a more generic error: + +`Error fetching` + +When you manually execute operations such as dropping or flushing impressions or events, an error is received if another operation is running at the same time. + +In Debug level, the following log appears for flushing: + +``` +Cannot execute flush. Another operation is performing operations in Events. +``` +and for impressions +``` +Cannot execute flush. Another operation is performing operations in Impressions. +``` +In Debug level, the following log appears for dropping: +``` +Cannot execute drop. Another operation is performing operations in Events. +``` +and for impressions +``` +Cannot execute drop. Another operation is performing operations in Impressions. +``` +Additionally, the Synchronizer performs automatic eviction for events and impressions. Manual and automatic eviction are not executed at the same time. In other words, if some eviction is running, the process skips the new operation. In Debug level, it informs the following message: + +``` +Another task is performing operations on Events. Skipping. +``` +and for impressions +``` +Another task is performing operations on Impressions. Skipping. +``` +### Webhook + +If you want to track messages using Slack,do it by adding the webhook URL and the slack channel into the configuration of Split Synchronizer. + +How you start your Synchronizer determines how you add this parameter: + +| **JSON** | **CLI PARAMETER** | **DOCKER ENV** | **TYPE** | **DESCRIPTION** | +|----------|-------------------|--------------------------|----------|-----------------------------------------------------------------------------------| +| channel | slack-channel | SPLIT_SYNC_SLACK_CHANNEL | string | Set the Slack channel or user to report a summary in realtime of ERROR log level. | +| webhook | slack-webhook | SPLIT_SYNC_SLACK_WEBHOOK | string | Set the Slack webhook URL to report a summary in realtime of ERROR log level. | + +With this webhook, you can track error level messages, when the Split Synchronizer starts, when the Split Synchronizer is gracefully shut down, or if it was forced to stop. + +### Checking to see if Split Synchronizer is configured properly + +The following are a set of config keys that are used to adjust the settings for more appropriate ones if your workload demands a custom configuration. The defaults keep the memory consumption constant, even at high load; however, not all workloads are similar. This guide provides instructions for measuring the capacity of the Synchronizer process and fine-tuning it if the default settings are not sufficient. With version 5 of the Synchronizer, this is still a valid check but the result should be close to 0 at all times because it constantly evicts impressions and events with this new version. + +Fp: frequency per post determined by ImpressionsPostRate and EventsPostRate + +Ap: amount of Impressions or Events per post, determined by ImpressionsPerPost and EventsPerPost + +T# = Number of threads used for sending data + +T(h) = (3,600/Fp) \* Ap \* T# + +This is the total amount of impressions or events flushed per hour. + +Es = Events generated per second. + +Eh = Es x 3,600 (Events generated per hour) + +Is = Impressions generated per second + +Ih = Is x 3,600 (Impressions generated per hour) + +Xh = Depending on what you are calculating, use either Ih or Eh to analyze Impressions or Events respectively. Using the previously described definitions, replace them with their values below to calculate lambda(ℷ) and analyze its value according to the description below: + +ℷ = T(h) / Xh + +If ℷ \>= 1: The current configuration is processing events or impressions without keeping elements in the stack. In other words, eviction rate \>= generation rate. Split Synchronizer is able to flush data as it arrives in the system from the SDKs. + +If ℷ \< 1: The current configuration may not be enough to process all the data coming in, and over time, it may produce an always-increasing memory footprint. The recommendation is to increase the number of threads or reduce the frequency for evicting elements. We recommend increasing the number of threads if they are still using the default value of 1, and to not exceed the number of cores. However, when reducing the frequency of element eviction (flush operation), decrease the value in a conservative manner by increments of ten or twenty percent each time. + +**Example 1** + +We calculate the performance of Impressions considering the following +configuration scenario: + +- Impressions Post Rate (Fp) = 60 Seconds + +- Impressions Per Post (Ap) = 1,000 + +- Impressions generated per second (Is) = 3 + +- Impressions generated in one hour (Ih) = 3\*3,600 = 10,800 + +- Number of threads (T#) = 1 + +Let's do some math. The total amount of impressions sent per hour is driven by: + +3,600 seconds in one hour / Fp \* Ap \* T1 or T(h) = (3,600/60) \* 1000 \* 1 = 60,000 + +being Xh = Ih = 10,800 (how many impressions per hour). + +Then our ℷ factor is determined by + +ℷ = T(h) / Xh = 60,000 / 10,800 = 5,555 + +\- ℷ is higher than 1: The configuration above supports some peaks and send all the impressions. + +**Example 2** + +Now let’s consider a higher number of Impressions per hour with the same configuration as before: + +- Impressions Post Rate (Fp) = 60 Seconds + +- Impressions Per Post (Ap) = 1,000 + +- Impressions generated per second (Is) = 30 + +- Impressions generated in one hour (Ih)= 30\*3,600 = 108,000 + +- Number of threads (T#) = 1 + +Let's do more math: + +\- T(h) = (3,600/60) \* 1000 \* 1 = 60,000 + +\- Xh = Ih = 108,000 + +\- ℷ = T(h) / Xh = 60,000 / 108,000 = 0.55 + +\- ℷ is less than 1: The configuration above is not enough to flush impressions. It needs more than one hour to evict and send all the elements to the Split servers. However, the Synchronizer continues to receive elements over the next hour. In this case, the corrective action is increasing the number of threads if the default is one. Then, proceed to decrease the rate of flush as indicated in the previous section. + +- T# = 2 or Fp = 30 (is good enough) + +Note: Eviction could also be executed manually, but keep in mind that this is a manual task that sends 5 batches of 5,000 elements (25,000 in total per call). For this case, 2 calls to that manual eviction need to execute to evict the 48,000 pending impressions. + +**Example 3** + +Let's try with a higher number of Impressions generated per hour: + +- Impressions Post Rate (Fp) = 60 Seconds + +- Impressions Per Post (Ap) = 1,000 + +- Impressions generated per second (Is) = 300 + +- Impressions generated in one hour (Ih)= 300\*3,600 = 1,080,000 + +- Number of threads (T#) = 2 + +Let's do more math: + +\- T(h) = (3,600/60) \* 1000 \* 2 = 120,000 + +\- Xh = Ih = 1,080,000 + +\- ℷ = T(h) / Xh = 120,000 / 1,080,000 = 0.11 + +\- ℷ is less than 1: This indicates that the process is not adequately provisioned to successfully send all generated impressions. If this is only a peak, it takes more than 8 hours to send all of the impressions to Split servers (120,000 impressions are sent per hour), even if using more than one thread. Manual eviction is also not the solution. In this case, editing the configuration is a better approach or strategy to follow. + +T# = 5 and Fp = 15 could be an example of making sure that the generation rate will be less than the eviction rate. + +## Deploying Synchronizer to AWS ECS securely + +To create a task definition that doesn't reveal API keys and passwords in the environment, create parameters in [AWS Systems Manager Parameter store](https://aws.amazon.com/premiumsupport/knowledge-center/ecs-data-security-container-task/), then reference their ARN when adding the ECS environment variables: + +Upgrading the Synchronizer + +To upgrade the Synchronizer, do the following: + +- Stop Split Sync + [gracefully](https://help.split.io/hc/en-us/articles/360019686092-Split-synchronizer-proxy#service-shutdown) + +- Upgrade Split Sync binary + +- Start the service again + +- Watch the logs for a short period of time to make sure no warnings arise. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-agents/_category_.json b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-agents/_category_.json new file mode 100644 index 00000000000..73ad8da9d25 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-agents/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Client-side Agents", + "collapsible": "true", + "collapsed": "true", + "className": "red", + "position": 5 +} \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-agents/android-rum-agent.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-agents/android-rum-agent.md new file mode 100644 index 00000000000..32a686a61cf --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-agents/android-rum-agent.md @@ -0,0 +1,381 @@ +--- +title: Android RUM Agent +sidebar_label: Android RUM Agent +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +This guide provides detailed information about Split's Real User Monitoring (RUM) Agent for Android. + +Split's Android RUM Agent collects events about your users' experience when they use your application and sends this information to Split services. This allows you to measure and analyze the impact of feature flag changes on performance metrics. + +## Language Support + +Split's Android RUM Agent is designed for Android applications written in Java or Kotlin and is compatible with Android SDK versions 15 and later (4.0.3 Ice Cream Sandwich). + +## Initialization + +Set up Split's RUM Agent in your code with the following three steps: + +### 1. Import the Agent into your project + +Import the Agent into your project with the following line: + + + +```groovy +implementation 'io.split.client:android-rum-agent:0.4.0' +``` + + +```kotlin +implementation("io.split.client:android-rum-agent:0.4.0") +``` + + + +### 2. Setup the Agent + +To allow the Agent to send information to Split services, specify the SDK through your app's `AndroidManifest.xml` file, as a `meta-data` tag inside the `application` tag. + +```xml title="AndroidManifest initialization" + + + + ... + + + + + +``` + +Alternatively, you can call the `setup` method on the `SplitRumAgent` object. This value will override the one specified through the `AndroidManifest.xml`. + + + +```java +SplitRumAgent.setup("YOUR_SDK_KEY"); +``` + + +```kotlin +SplitRumAgent.setup("YOUR_SDK_KEY") +``` + + + +### 3. Add an Identity + +While the Agent will work without having an Identity, events won't be sent to Split services until at least one is set. + +Identity objects consist of a key and a [traffic type](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type). The traffic type value must match the name of a traffic type that you have defined in the Split Management Console. + +The RUM Agent provides methods to manage Identities, as shown below: + + + +```java +// add one Identity +SplitRumAgent.addIdentity(new Identity("my_user", "user")); + +// add multiple Identities +SplitRumAgent.addIdentities( + new Identity("my_user", "user"), + new Identity("my_account", "account") +); + +// remove one Identity +SplitRumAgent.removeIdentity(new Identity("my_user", "user")); + +// remove all Identities +SplitRumAgent.removeIdentities(); +``` + + +```kotlin +// add one Identity +SplitRumAgent.addIdentity(Identity("my_user", "user")) + +// add multiple Identities +SplitRumAgent.addIdentities( + Identity("my_user", "user"), + Identity("my_account", "account") +) + +// remove one Identity +SplitRumAgent.removeIdentity(Identity("my_user", "user")) + +// remove all Identities +SplitRumAgent.removeIdentities() +``` + + + +## Configuration + +Split's Android RUM Agent can be configured to change its default behavior. The following options are available: +- Log Level: Level of logging. Valid values are `DEBUG`, `INFO`, `WARNING` and `ERROR`. +- Prefix: Optional prefix to append to the `eventTypeId` of the events sent to Split. For example, if you set the prefix to "my-app", the event type "error" will be sent as "my-app.error". The prefix "split.rum" is used by default if no prefix is configured. +- User Consent: User consent status used to control the tracking of events. Possible values are `GRANTED`, `DECLINED`, and `UNKNOWN`. The default value is `'GRANTED'`. See the [User consent](#user-consent) section for details. + + These options can be configured using the `AndroidManifest.xml` or programmatically. Values specified programmatically will override values set through the `AndroidManifest.xml`. Both methods are demonstrated below. + +Configuration using the manifest file: + +```xml titlel="AndroidManifest.xml configuration" + + + + ... + + + + + + + + + + +``` + +Configuration specified programatically: + + + +```java +SplitRumConfiguration config = new SplitRumConfiguration.Builder() + .setPrefix("pre") + .setLogLevel(LogLevel.DEBUG) + .build(); + +SplitRumAgent.setup("YOUR_SDK_KEY", config); +``` + + +```kotlin +val config = SplitRumConfiguration.Builder() + .setPrefix("pre") + .setLogLevel(LogLevel.DEBUG) + .build() + + SplitRumAgent.setup("YOUR_SDK_KEY", config) +``` + + + +## Events + +Split's Android RUM Agent collects a number of events by default. + +### Default events + +| **Event type ID** | **Description** | **Has value?** | **Has properties?** | +| --- | --- | --- | --- | +| crash | Any unhandled exception that causes the application to crash. | No | ```{ message: string, trace: string }``` | +| error | Exceptions tracked via the `trackError` method. | No | ```{ message: string, trace: string }``` | +| app_start | Time in milliseconds elapsed until app is launched | Yes | No | +| anr | Sent if an ANR (Application Not Responding) is detected. | No | No | +| device_info | Information provided by the OS about a device. This is sent the first time the Agent runs on the device. | No | ```{ id: string, display: string, product: string, device: string, board: string, manufacturer: string, brand: string, model: string, hardware: string, base: string, incremental: string, sdkVersion: string, host: string, fingerprint: string, release: string, baseOs: string, socManufacturer: string, securityPatch: string, abis: string }``` | + +### Automatic metric creation + +Split will automatically create [metrics](https://help.split.io/hc/en-us/articles/22005565241101-Metrics) for a subset of the event types received from the Android RUM Agent. These "out of the box metrics" are auto-created for you: + +| **Event type** | **Metric name** | +| --- | --- | +| split.rum.error | Count of Application Errors - Split Agents | +| split.rum.crash | Count of Crashes - Split Agents | +| split.rum.app_start | Average App Start Time - Split Agents | +| split.rum.anr | Count of ANRs - Split Agents | + +For a metric that was auto-created, you can manage the [definition](https://help.split.io/hc/en-us/articles/22005565241101-Metrics) and [alert policies](https://help.split.io/hc/en-us/articles/19832312225293-Configuring-metric-alerting) like you would for any other metric. If you delete a metric that was auto-created, Split will not re-create the metric, even if the event type is still flowing. + +## Advanced use cases + +### Custom properties + +Each event for the metrics described above automatically includes a `session_id` property that can be use to filter certain events when defining Split metrics for experimentation purposes. Learn more about [metric definitions and how to define property filters](https://help.split.io/hc/en-us/articles/22005565241101-Metrics) + +| **name** | **Description** | +| --- | --- | +| session_id | ID of the session in which the event took place. | + +Custom properties can be also added to a tracked event, as shown below: + + + +```java +// add a single property +SplitRumAgent.setProperty("property_name", "property_value"); + +// add multiple properties +Map newProperties = new HashMap<>(); +newProperties.put("property_name", "property_value"); +newProperties.put("property_name_2", "property_value_2"); +SplitRumAgent.setProperties(newProperties); + +// get all properties +Map properties = SplitRumAgent.getProperties(); + +// remove a single property +SplitRumAgent.removeProperty("property_name_2"); + +// remove all properties +SplitRumAgent.removeProperties(); +``` + + +```kotlin +// add a single property +SplitRumAgent.setProperty("property_name", "property_value") + +// add multiple properties +SplitRumAgent.setProperties( + mapOf( + "property_name" to "property_value", + "property_name_2" to "property_value_2", + ) +) + +// get all properties +val properties: Map = SplitRumAgent.getProperties() + +// remove a single property +SplitRumAgent.removeProperty("property_name_2") + +// remove all properties +SplitRumAgent.removeProperties() +``` + + + +### Custom events + +Custom events can be tracked using the following options: +- the `track` method +- the specialized `trackError` method +- the specialized `trackTimeFromStart` +These methods are demonstrated below. + +Using the `track` methods: + + + +```java +Map properties = new HashMap<>(); +properties.put("property_name", "property_value"); + +// Track event +SplitRumAgent.track("event_type_id"); + +// Track event with value +SplitRumAgent.track("event_type_id", 100L); + +// Track event with properties +SplitRumAgent.track("event_type_id", properties); + +// Track event with value and properties +SplitRumAgent.track("event_type_id", 100L, properties); +``` + + +```kotlin +// Track event +SplitRumAgent.track("event_type_id") + +// Track event with value +SplitRumAgent.track("event_type_id", 100) + +// Track event with properties +SplitRumAgent.track("event_type_id", mapOf("property_name" to "property_value")) + +// Track event with value and properties +SplitRumAgent.track("event_type_id", 100, mapOf("property_name" to "property_value")) +``` + + + +Using the `trackError` method: + + + +```java +SplitRumAgent.trackError(new Throwable("my_error")); +``` + + +```kotlin +SplitRumAgent.trackError(Throwable("my_error")) +``` + + + +Using the `trackTimeFromStart` method. This method generates an event which value is the number of milliseconds between app launch and the moment the method is called. + + + +```java +SplitRumAgent.trackTimeFromStart("content_loaded"); +``` + + +```kotlin +SplitRumAgent.trackTimeFromStart("content_loaded") +``` + + + +### User consent + +By default the Agent will send events to Split cloud, but you can disable this behavior until user consent is explicitly granted. + +The `userConsent` configuration parameter lets you set the initial consent status of the Agent, and the `SplitRumAgent.setUserConsent(boolean)` method lets you grant (enable) or decline (disable) dynamic event tracking. + +There are three possible initial states: + * `'GRANTED'`: The user grants consent for tracking events. The Agent sends them to Split cloud. This is the default value if `userConsent` param is not defined. + * `'DECLINED'`: The user declines consent for tracking events. The Agent does not send them to Split cloud. + * `'UNKNOWN'`: The user neither grants nor declines consent for tracking events. The Agent tracks them in its internal storage, and eventually either sends them or not if the consent status is updated to `'GRANTED'` or `'DECLINED'` respectively. + +The status can be updated at any time with the `setUserConsent` method. + +Working with user consent is demonstrated below. + +```javascript title="User consent: Initial config, getter and setter" +SplitRumAgent.setup('YOUR_SDK_KEY', { + // Overwrites the initial consent status of the Agent, which is 'GRANTED' by default. + // 'UNKNOWN' status represents that the user has neither granted nor declined consent for tracking data, + // so the Agent will locally track data but not send it to Split cloud until consent is changed to 'GRANTED'. + userConsent: 'UNKNOWN' +}); + +// `getUserConsent` method returns the current consent status. +SplitRumAgent.getUserConsent() == UserConsent.GRANTED; + +// `setUserConsent` method lets you update the consent status at any time. +// Pass `true` for 'GRANTED' and `false` for 'DECLINED'. +SplitRumAgent.setUserConsent(true); // Consent status changed from 'UNKNOWN' to 'GRANTED'. Data will be sent to Split cloud. +SplitRumAgent.getUserConsent() == UserConsent.GRANTED; + +SplitRumAgent.setUserConsent(false); // Consent status changed from 'GRANTED' to 'DECLINED'. Data will not be sent to Split cloud. +SplitRumAgent.getUserConsent() == UserConsent.DECLINED; +``` + + + \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-agents/browser-rum-agent.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-agents/browser-rum-agent.md new file mode 100644 index 00000000000..782630153ac --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-agents/browser-rum-agent.md @@ -0,0 +1,494 @@ +--- +title: Browser RUM Agent +sidebar_label: Browser RUM Agent +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +This guide provides detailed information about Split's Real User Monitoring (RUM) Agent for Web browsers. + +Split's Browser RUM Agent collects events about your users' experience when they visit your web application and sends this information to Split services. This allows you to measure and analyze the impact of feature flag changes on performance metrics. + +:::info[Migrating from v0.x to v1.x] +When upgrading, consider that the `webVitals` event collector (`import { webVitals } from '@splitsoftware/browser-rum-agent';`) is not exported anymore, but registered by default. + +In case you were registering it with specific options, you can now pass the options in the `eventCollectors` property of the `setup` configuration object. + +For example, replace this: + +```javascript +import { SplitRumAgent, webVitals } from '@splitsoftware/browser-rum-agent'; + +SplitRumAgent.register(webVitals(WEB_VITALS_OPTIONS)); + +SplitRumAgent.setup(YOUR_SDK_KEY); +``` + +With this: + +```javascript +import { SplitRumAgent } from '@splitsoftware/browser-rum-agent'; + +SplitRumAgent.setup(YOUR_SDK_KEY, { + eventCollectors: { webVitals: WEB_VITALS_OPTIONS } +}); +``` + +See the [Web Vitals](#web-vitals) section for more information about Google Web Vitals metrics collected by the Browser RUM Agent. +::: + +## Language Support + +Split's Browser RUM Agent is compatible with EcmaScript 5 syntax and therefore supports the majority of today's popular browsers, with the exception of older browsers like IE. We rely on the browser [Beacon API](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon) support to reliably send information to Split services for processing. For browsers that do not support Beacon API, the RUM Agent defaults to using [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) or [XHR](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) instead. + +Additional Web APIs like Promise, History and Performance APIs, highly available in modern browser, are required to collect some specific events, but not for the regular operation of the Agent. See the [Events section](#events) for specific events and their compatibility. + +## Initialization + +Set up Split's RUM Agent in your code with the following two steps: + +### 1. Import the Agent into your project + +Split's RUM Agent is delivered as a NPM package and as a script UMD bundle hosted in a CDN. You can import the Agent into your project using either of the two methods, as shown below. + + + +```bash +npm install --save @splitsoftware/browser-rum-agent +``` + + +```html + +``` + + + +### 2. Setup the Agent + +You can initialize the Browser RUM Agent in your code as shown below. + + + +```javascript +import { SplitRumAgent } from '@splitsoftware/browser-rum-agent'; + +SplitRumAgent + .setup('YOUR_SDK_KEY') + .addIdentities([ + { key: 'key', trafficType: 'a_traffic_type' }, + { key: 'another_key', trafficType: 'another_traffic_type' } + ]); +``` + + +```javascript +const { SplitRumAgent } = require('@splitsoftware/browser-rum-agent'); + +SplitRumAgent + .setup('YOUR_SDK_KEY') + .addIdentities([ + { key: 'key', trafficType: 'a_traffic_type' }, + { key: 'another_key', trafficType: 'another_traffic_type' } + ]); +``` + + +```html + + + +``` + + + +Alternatively, you can initialize the Agent in two parts. First, import the Agent early in the code execution order, and then postpone setting the identities until that information is available. + + + +```javascript +import { SplitRumAgent } from '@splitsoftware/browser-rum-agent'; +SplitRumAgent.setup('YOUR_SDK_KEY'); + +// In a different file or part of your code, where the identities are available: +SplitRumAgent.addIdentity({ key: 'user_id', trafficType: 'user' }); +``` + + +```html + + + + + +``` + + + +Identity objects consist of a key and a [traffic type](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type). The traffic type value must match the name of a traffic type that you have defined in the Split Management Console. + +These identities are used to associate the events captured by the RUM Agent to some user, before sending them to Split services. If you provide more than one identity, the captured events will be duplicated and sent to Split services for each identity. + +## Configuration + +The RUM Agent can be configured to change its default behavior. The following options are available: +- Prefix: Optional prefix to append to the `eventTypeId` of the events sent to Split. For example, if you set the prefix to `'my-app'`, the event type `'error'` will be sent as `'my-app.error'`. It defaults to `'split.rum'`. +- Push Rate: The Agent posts the queued events data in bulks. This parameter controls the posting rate in seconds. The default value is `30`. +- Queue Size: The maximum number of event items we want to queue. If we queue more values, events will be dropped until they are sent to Split. The default value is `5000`. +- User Consent: User consent status used to control the tracking of events and impressions. Possible values are `'GRANTED'`, `'DECLINED'`, and `'UNKNOWN'`. The default value is `'GRANTED'`. See the [User consent](#user-consent) section for details. + +These options can be configured programmatically, as demonstrated below: + +```javascript +SplitRumAgent.setup('YOUR_SDK_KEY', { + prefix: 'my-app', + pushRate: 30, + queueSize: 5000, + userConsent: 'GRANTED' +}); +``` + +## Events + +Split's RUM Agent collects a number of browser events by default and can be extended by registering *event collectors*. Event collectors collect additional events that are relevant to your application. They are not shipped by default with the Agent itself to avoid increasing your bundle size with unnecessary code. + +Event collectors are available when using the NPM package, or with a "full" version of the UMD bundle hosted in our CDN. They can be imported and registered as follows: + + + +```javascript +import { SplitRumAgent, routeChanges } from '@splitsoftware/browser-rum-agent'; + +SplitRumAgent.register(routeChanges()); +``` + + +```html + + +``` + + + +Refer to the table below and the following sections for more information about the default events and the available event collectors. + +### Default events + +| **Event type ID** | **Description** | **Has value?** | **Has properties?** | +| --- | --- | --- | --- | +| error | Any JavaScript unhandled error and promise rejection | No | ```{ message: string, stack: string }``` | +| page.load.time | Time in milliseconds elapsed until the document is fully loaded and parsed. It is equivalent to the time until the [load event](https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event) is fired. | Yes | No | +| time.to.dom.interactive | Time in milliseconds until the document is ready and before the full page load time. If this time is high, it usually implies that the critical rendering path is complex and that the download of resources will start later. Related to the [domInteractive](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming/domInteractive) property. | Yes | No | + +#### Web Vitals + +[Web Vitals](https://web.dev/vitals/) is an initiative by Google to provide unified guidance for quality signals that are essential to delivering a great user experience on the web. + +The RUM Agent uses the Google [web-vitals NPM package](https://www.npmjs.com/package/web-vitals) to collect the Web Vitals metrics. + +By default, the Agent will collect all metrics supported by the web-vitals package: +* **Core Web Vitals:** + - [Cumulative Layout Shift (CLS)](https://web.dev/cls/) + - [Interaction to Next Paint (INP)](https://web.dev/inp/) + - [Largest Contentful Paint (LCP)](https://web.dev/lcp/) +* **Other metrics:** + - [First Contentful Paint (FCP)](https://web.dev/fcp/) + - [Time to First Byte (TTFB)](https://web.dev/ttfb/) + - [First Input Delay (FID)](https://web.dev/fid/) + +You can also configure the Agent to collect only a subset of the web-vitals: + +```javascript +SplitRumAgent.setup('YOUR_SDK_KEY', { + eventCollectors: { + webVitals: { + reportOptions: { + // collects only the core web-vitals + onCLS: true, + onINP: true, + onLCP: true, + // other web-vital metrics are not collected + onFCP: false, + onTTFB: false, + onFID: false + } + } + } +}); +``` + +The format of collected events is shown below: + +```typescript +type WebVitalsEvent = { + eventTypeId: 'webvitals.cls' | 'webvitals.inp' | 'webvitals.lcp' | 'webvitals.fcp' | 'webvitals.ttfb' | 'webvitals.fid', + value: number, // value in milliseconds + properties: { + rating: 'good' | 'needsImprovement' | 'poor', + navigationType: 'navigate' | 'reload' | 'back_forward' | 'back-forward-cache' | 'prerender' | 'restore' + } +} +``` + +### Time to Interactive + +[Time to Interactive (TTI)](https://web.dev/tti/) is a metric that measures the time from when the page starts loading to when its main sub-resources have loaded and it is capable of reliably responding to user input. + +The RUM Agent exports an event collector for TTI, which internally uses the [tti-polyfill NPM package](https://www.npmjs.com/package/tti-polyfill) to collect it. + +You can set it as follows: + +```javascript +import { SplitRumAgent, tti } from '@splitsoftware/browser-rum-agent'; + +SplitRumAgent.register(tti()); +``` + +Unlike `webVitals`, the `tti` collector does not support any configuration options. + +The format of collected event is shown below: + +```typescript +type TTIEvent = { + eventTypeId: 'time.to.interactive', + value: number, // TTI value in milliseconds +} +``` + +### Route Changes + +Route changes are events that are triggered when the user navigates to a new page in a Single-Page Application (SPA). + +The RUM Agent exports an event collector for route changes. + +You can set the Agent to collect route change events as follows: + +```javascript +import { SplitRumAgent, routeChanges } from '@splitsoftware/browser-rum-agent'; + +SplitRumAgent.register(routeChanges()); +``` + +You can also configure the Agent to collect only a subset of the route changes, by providing a filter callback. For example, to ignore hash (fragment) changes: + +```javascript +SplitRumAgent.register(routeChanges({ + filter({ fromUrl, toUrl }) { + return fromUrl.split('#')[0] !== toUrl.split('#')[0]; + } +})); +``` + +The format of a collected event is shown below: + +```typescript +type RouteChangesEvent = { + eventTypeId: 'route.change', + value: number | undefined, // Value of the `duration` property + properties: { + // URL value before the change, without including the origin (protocol, host and port) + fromUrl: string, + // URL value after the change, without including the origin (protocol, host and port) + toUrl: string, + // Type of URL change + // * `pushState` indicates a new entry added to the history stack, when `history.pushState` is called + // * `replaceState` indicates the entry at the current index in the history stack being replaced, when `history.replaceState` is called + // * `popstate` indicates a change to an arbitrary index in the history stack + historyChangeSource: 'pushState' | 'replaceState' | 'popstate', + + /* Browsers that support the Performance Timeline API will include the following properties: */ + // Estimated duration of the navigation transition in milliseconds. The calculation is based on a time window of long tasks and resources around the history change event. + duration?: number, + // Value of performance.now() when the navigation started + startTime?: number, + // Time spent on the previous route (`fromUrl`) in milliseconds + timeOnRoute?: number, + } +} +``` + +## Automatic metric creation + +Split will automatically create [metrics](https://help.split.io/hc/en-us/articles/22005565241101-Metrics) for a subset of the event types received from the Browser RUM Agent. These "out of the box metrics" are auto-created for you: + +| **Event type** | **Metric name** | +| --- | --- | +| split.rum.error | Count of Application Errors - Split Agents | +| split.rum.page.load.time | Average Page Load Time - Split Agents | +| split.rum.webvitals.cls | Average CLS - Split Agents | +| split.rum.webvitals.inp | Average INP - Split Agents | +| split.rum.webvitals.lcp | Average LCP - Split Agents | +| split.rum.webvitals.fcp | Average FCP - Split Agents | +| split.rum.webvitals.ttfb | Average TTFB - Split Agents | +| split.rum.webvitals.fid | Average FID - Split Agents | + +For a metric that was auto-created, you can manage the [definition](https://help.split.io/hc/en-us/articles/22005565241101-Metrics) and [alert policies](https://help.split.io/hc/en-us/articles/19832312225293-Configuring-metric-alerting) like you would for any other metric. If you delete a metric that was auto-created, Split will not re-create the metric, even if the event type is still flowing. + +## Advanced use cases + +### Custom properties + +Each event for the metrics described above automatically includes three properties that can be use to filter certain events when defining Split metrics for experimentation purposes. Learn more about [metric definitions and how to define property filters](https://help.split.io/hc/en-us/articles/22005565241101-Metrics). + +| **Name** | **Description** | **Values** | +| --- | --- | --- | +| connectionType | Speed of connection | 2g, 3g, 4g | +| url | The url that generated the metric | | +| userAgent | The user agent | | + +Custom properties can be also added to a tracked event by using the `setProperties` method: + +```javascript +SplitRumAgent.setProperties({ 'property_name': 'property_value' }); // set a single or multiple properties as a map object of key/value pairs + +const properties = SplitRumAgent.getProperties(); // get all properties as a map object of key/value pairs + +SplitRumAgent.removeProperties(); // remove properties +``` + +### Custom events + +There are multiple methods to track custom events: +- using the `track` method +- using the specialized `trackError` method +- registering a custom event collector +These methods are demonstrated below. + +Using the `track` method: + +```javascript +SplitRumAgent.track('event_type_id'); // track a single event without value +SplitRumAgent.track('event_type_id', 100); // track a single event with value +SplitRumAgent.track('event_type_id', 100, { 'property_name': 'property_value' }); // track a single event with value and properties +``` + +Using the `trackError` method: + +```javascript +SplitRumAgent.trackError('error_message'); // Shorthand for track('error', undefined, { message: 'error_message', stack: 'unavailable' }) +SplitRumAgent.trackError(errorObject); // Shorthand for track('error', undefined, { message: error.message, stack: error.stack }) +``` + +Registering a custom event collector: + +```javascript +SplitRumAgent.register(({ track }) => { + track({ + eventTypeId: 'event_type_id', + value: 100, // optional value + properties: { 'property_name': 'property_value' } // optional properties + }) +}); +``` + +### Async/Lazy loading + +Including the RUM Agent code in your application bundle will increase its size and impact the captured performance metrics. + +If you want to avoid this, you can load the Agent asynchronously by [lazy loading](https://developer.mozilla.org/en-US/docs/Web/Performance/Lazy_loading) it, for example by using dynamic imports or async script tags: + + + + +```javascript +import('@splitsoftware/browser-rum-agent').then(({ SplitRumAgent }) => { + SplitRumAgent + .setup('YOUR_SDK_KEY') + .addIdentity({ key: 'user_id', trafficType: 'user' }); +}).catch(err => { + console.error('Error loading Agent', err); +}); +``` + + +```html + + +``` + + + +:::info[Missing error events on lazy loading] +When using lazy loading, the RUM Agent will normally not be able to capture any errors that occur before the Agent has finished loading. To solve this, you can place the following script tag in the `` section of your page. + +```html + +``` + +This script captures regular JavaScript errors and unhandled promise rejections, and stores them in memory. Once the RUM Agent loads, it sends the captured errors to Split services for processing, ensuring that even errors occurring before the Agent is fully loaded are not missed. +::: + +### User consent + +By default the Agent will send events to Split cloud, but you can disable this behavior until user consent is explicitly granted. + +The `userConsent` configuration parameter lets you set the initial consent status of the Agent, and the `SplitRumAgent.setUserConsent(boolean)` method lets you grant (enable) or decline (disable) dynamic event tracking. + +There are three possible initial states: + * `'GRANTED'`: The user grants consent for tracking events. The Agent sends them to Split cloud. This is the default value if `userConsent` param is not defined. + * `'DECLINED'`: The user declines consent for tracking events. The Agent does not send them to Split cloud. + * `'UNKNOWN'`: The user neither grants nor declines consent for tracking events. The Agent tracks them in its internal storage, and eventually either sends them or not if the consent status is updated to `'GRANTED'` or `'DECLINED'` respectively. + +The status can be updated at any time with the `setUserConsent` method. + +Working with user consent is demonstrated below. + +```javascript title="User consent: Initial config, getter and setter" +SplitRumAgent.setup('YOUR_SDK_KEY', { + // Overwrites the initial consent status of the Agent, which is 'GRANTED' by default. + // 'UNKNOWN' status represents that the user has neither granted nor declined consent for tracking data, + // so the Agent will locally track data but not send it to Split cloud until consent is changed to 'GRANTED'. + userConsent: 'UNKNOWN' +}); + +// `getUserConsent` method returns the current consent status. +SplitRumAgent.getUserConsent() === 'UNKNOWN'; + +// `setUserConsent` method lets you update the consent status at any time. +// Pass `true` for 'GRANTED' and `false` for 'DECLINED'. +SplitRumAgent.setUserConsent(true); // Consent status changed from 'UNKNOWN' to 'GRANTED'. Data will be sent to Split cloud. +SplitRumAgent.getUserConsent() === 'GRANTED'; + +SplitRumAgent.setUserConsent(false); // Consent status changed from 'GRANTED' to 'DECLINED'. Data will not be sent to Split cloud. +SplitRumAgent.getUserConsent() === 'DECLINED'; +``` + + + +## Example apps + +The following repository contains different example apps that demonstrate how to use Split's Browser RUM Agent: + +* [Browser RUM Agent examples](https://github.com/splitio/browser-rum-agent-examples) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-agents/ios-rum-agent.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-agents/ios-rum-agent.md new file mode 100644 index 00000000000..2ccc5e01525 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-agents/ios-rum-agent.md @@ -0,0 +1,259 @@ +--- +title: iOS RUM Agent +sidebar_label: iOS RUM Agent +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +This guide provides detailed information about Split's Real User Monitoring (RUM) Agent for iOS. + +Split's iOS RUM Agent collects events about your users' experience when they use your application and sends this information to Split services. This allows you to measure and analyze the impact of feature flag changes on performance metrics. + +## Language Support + +Split's iOS RUM Agent is designed for iOS applications written in Swift and is compatible with iOS SDK versions 12 and later. + +## Initialization + +Set up Split's RUM Agent in your code with the following three steps: + +### 1. Import the Agent into your project + +#### Swift Package Manager +Import the Agent into your project using Swift Package Manager pointing to the iOS RUM repo [URL](https://github.com/splitio/ios-rum). Currently, the last version is `0.4.0`. + +#### Cocoa Pods +You can also import the Agent into your Xcode project using CocoaPods, adding it in your **Podfile**. + +```swift title="Podfile" +pod 'SplitRum', '~> 0.4.0' +``` + +:::warning[Important] +If you get the `Sandbox: rsync.samba(19690) deny(1) file-read-data ...` error, make sure that the **ENABLE_USER_SCRIPT_SANDBOXING** project flag is set to 'No' +::: + +### 2. Setup the Agent + +To allow the Agent to send information to Split services, you need to call the `setup` method on the `SplitRum` object. + +```swift title="Swift" +try? SplitRum.setup(apiKey: "YOUR_SDK_KEY") +``` + +:::warning[Important] +The Crashlytics framework has a compatibility issue that interferes with the proper functioning of Split RUM Agent when Crashlytics is initialized first. To resolve this, initialize the Split RUM Agent using the setup method before starting Crashlytics. + +```swift + try? SplitRum.setup(apiKey: "YOUR_SDK_KEY") + FirebaseApp.configure() +``` +::: + +Alternatively, you can create a `SplitRumAgent-Info.plist` file with a key called `apiKey` and your SDK KEY as its value. Then call the `setup` method without parameters. + +```xml title="SplitRumAgent-Info.plist configuration" + + + + + apiKey + YOUR_SDK_KEY + + +``` + +```swift title="Swift" +try? SplitRum.setup() +``` + +Arguments passed to the `setup` method will override any value contained in the `SplitRumAgent-Info.plist`. When initializing the Agent, the `setup` method will throw an error if any parameter invalid, so this method must be called using a `try` clause. + +### 3. Add an Identity + +While the Agent will work without having an Identity, events won't be sent to Split services until at least one is set. + +Identity objects consist of a key and a [traffic type](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type). You can only pass values that match the names of traffic types already defined in the Split Management Console. + + The RUM Agent provides methods to manage Identities, as shown in the table below. + +```swift title="Swift" +// add one Identity +SplitRum.addIdentity(key: "my_user", trafficType: "user") + +// add multiple Identities +SplitRum.add(identities: [ + SplitIdentity(key: "user_key1", trafficType: "user"), + SplitIdentity(key: "user_key2", trafficType: "user") + ]) + +// remove one Identity +SplitRum.removeIdentity(key: "my_user", trafficType: "user") + +// remove all Identities +SplitRum.removeIdentities() +``` + +## Configuration + +Split's iOS RUM Agent can be configured to change its default behavior. The following options are available: +- Log Level: level of logging. Valid values are `VERBOSE`, `DEBUG`, `INFO`, `WARNING`, `ERROR` and `NONE`. + + Log level can be configured using the `SplitRumAgent-Info.plist` file or programmatically. Values specified programmatically will override any of the same values specified in the configuration file. + +Configuration using the manifest file: + +```xml titlel="AndroidManifest.xml configuration" + + + + + apiKey + YOUR_SDK_KEY + logLevel + DEBUG + + +``` + +Configuration specified programmatically: + +```swift title="Swift" +let config = SplitRumConfig().logLevel(.verbose) +SplitRum.setup(apiKey: "YOUR_SDK_KEY", config: config) +``` + +## Events + +Split's iOS RUM Agent collects a number of events by default. + +### Default events and properties + +| **Event type ID** | **Description** | **Has value?** | **Has properties?** | +| --- | --- | --- | --- | +| crash | Any unhandled error that causes the application to crash. | No | ```{ signal: int, message: string }``` | +| error | Errors tracked via the `trackError` method. | No | ```{ errorCode: int, errorMessage: string, isFatal: boolean, errorType: string, errorMethod: string }``` | +| app_start | Time in milliseconds elapsed until app is launched. | Yes | No | +| hangs | Sent if an application hang is detected. | No | No | +| device_info | Information provided by the OS about a device. This is sent the first time the Agent runs on the device. The device ID recorder is the ASIdentifierManager.shared().advertisingIdentifier value. | No | ```{ id: string, model: string, osName: string, osVersion: string }``` | + +Each event for the metrics described above automatically includes a `session_id` property that can be use to filter certain events when defining Split metrics for experimentation purposes. Learn more about [metric definitions and how to define property filters](https://help.split.io/hc/en-us/articles/22005565241101-Metrics). + +| **Name** | **Description** | +| --- | --- | +| session_id | ID of the session in which the event took place. | + +## Automatic metric creation + +Split will automatically create [metrics](https://help.split.io/hc/en-us/articles/22005565241101-Metrics) for a subset of the event types received from the iOS RUM Agent. These "out of the box metrics" are auto-created for you: + +| **Event type** | **Metric name** | +| --- | --- | +| split.rum.error | Count of Application Errors - Split Agents | +| split.rum.crash | Count of Crashes - Split Agents | +| split.rum.app_start | Average App Start Time - Split Agents | +| split.rum.anr | Count of ANRs - Split Agents | + +For a metric that was auto-created, you can manage the [definition](https://help.split.io/hc/en-us/articles/22005565241101-Metrics) and [alert policies](https://help.split.io/hc/en-us/articles/19832312225293-Configuring-metric-alerting) like you would for any other metric. If you delete a metric that was auto-created, Split will not re-create the metric, even if the event type is still flowing. + +## Advanced use cases + +### Custom properties + +Custom properties can be also added to a tracked event by using the various methods for managing them. + +```swift title="Swift" +// add a single property +SplitRum.setProperty(name: "property_name", value: "property_value") + +// add multiple properties +SplitRum.set(properties: ["property_name_1": "property_value_1", "property_name_2": "property_value_2"]) + +// remove property +func remove(property: "property_name") +``` + +### Custom events + +Custom events can be tracked using the following options: +- the `track` method +- the specialized `trackError` method +These methods are demonstrated below. + +Using the `track` methods: + +```swift title="Swift" +// Examples +// Track event for an identity`` +let result = SplitRum.track(eventType: "event_type_id", + userKey: "user", + trafficType: "traffic_type", + value: 100.0, + properties: ["property_name_1": "value1", "property_name_2": "value2"]) + +// Track event with value and properties +let result = SplitRum.track(eventType: "event_type_id", + value: 0.1, + properties: ["property_name_1": "value1", "property_name_2": "value2"]) +``` + +Using the `trackError` method: + +```swift title="Swift" +let errorInfo = SplitErrorEvent(message: "Error reaching server", isFatal: false).method("fetchData").code(-100).type(MyErrorEnum.type) +let result = SplitRum.track(error: SplitErrorEvent(message: "Error tracked manually", isFatal: true)) +``` + +Using the `trackTimeFromStart` method. This method generates an event which value is the number of milliseconds between app launch and the moment the method is called. + +```swift title="Swift" +SplitRum.trackTimeFromStart(marker: "data_loaded") +``` + +### User consent + +By default the Agent will send events to Split cloud, but you can disable this behavior until user consent is explicitly granted. + +The `userConsent` configuration parameter lets you set the initial consent status of the Agent, and the `SplitRum.setUserConsent(boolean)` method lets you grant (enable) or decline (disable) dynamic event tracking. + +There are three possible initial states: + * `'GRANTED'`: The user grants consent for tracking events and impressions. The SDK sends them to Split cloud. This is the default value if `userConsent` param is not defined. + * `'DECLINED'`: The user declines consent for tracking events and impressions. The SDK does not send them to Split cloud. + * `'UNKNOWN'`: The user neither grants nor declines consent for tracking events and impressions. The SDK tracks them in its internal storage, and eventually either sends them or not if the consent status is updated to `'GRANTED'` or `'DECLINED'` respectively. + +The status can be updated at any time with the `setUserConsent` factory method. + +Working with user consent is demonstrated below. + +```swift title="User consent: Initial config, getter and setter" + // Overwrites the initial consent status of the factory instance, which is 'GRANTED' by default. + // 'UNKNOWN' status represents that the user has neither granted nor declined consent for tracking data, + let cfg = SplitRumConfig().userConsent(.unknown) + + // so the Agent locally tracks data but not send it to Split cloud until consent is changed to 'GRANTED'. + try? SplitRum.setup(apiKey: apiKey, config: config) + + // Changed User Consent status to 'GRANTED'. Data will be sent to Split cloud. + SlitRum.setUserConsent(enabled: true); + // Changed User Consent status to 'DECLINED'. Data will not be sent to Split cloud. + SlitRum.setUserConsent(enabled: false); + + // The 'getUserConsent' method returns User Consent status. + // We expose the constants for customer checks and tracking. + if (SlitRum.userConsent == UserConsent.declined) { + print("USER CONSENT DECLINED"); + } + if (SlitRum.userConsent == UserConsent.granted) { + print("USER CONSENT GRANTED"); + } + if (SlitRum.userConsent == UserConsent.unknown) { + print("USER CONSENT UNKNOWN"); + } +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/_category_.json b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/_category_.json new file mode 100644 index 00000000000..08d3f0770ed --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Client-side SDK examples", + "collapsible": "true", + "collapsed": "true", + "className": "red", + "position": 4 +} \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/_category_.json b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/_category_.json new file mode 100644 index 00000000000..a35d4fdcd07 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Client-side SDKs", + "collapsible": "true", + "collapsed": "true", + "className": "red", + "position": 3 +} \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/android-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/android-sdk.md new file mode 100644 index 00000000000..e74282ad251 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/android-sdk.md @@ -0,0 +1,1492 @@ +--- +title: Android SDK +sidebar_label: Android SDK +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +This guide provides detailed information about our Android SDK. All of our SDKs are open source. Go to our [Android SDK GitHub repository](https://github.com/splitio/android-client) to see the source code. + +## Language support + +This library is designed for Android applications written in Java or Kotlin and is compatible with Android SDK versions 19 and later (4.4 Kit Kat). + +:::warning[Important] + +Starting with `Android v3.0.0`, this SDK now relies on `WorkManager v2.7.1`. This requires your application to use at least `compileSdk 31`. + +If you haven't upgraded to use API 31, you can force the downgrade of the `WorkManager` dependency. +```groovy +implementation("androidx.work:work-runtime") { + version { + strictly("2.6.0") + } +} +``` +::: + +## Initialization + +To get started, set up Split in your code base with the following two steps. + +### 1. Import the SDK into your project + +Import the SDK into your project using the following line: + +```java title="Gradle" +implementation 'io.split.client:android-client:5.1.1' +``` + +### 2. Instantiate the SDK and create a new Split client + +The first time the SDK is instantiated, it starts background tasks to update an in-memory cache and in-storage cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds, depending on the size of the data. + +If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while it is in this intermediate state, it may not have the data necessary to run the evaluation. In this circumstance, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072). + +After the first initialization, the fetched data is stored. Further initializations fetch data from that cache and the configuration is immediately available. + +We recommend instantiating the Split factory once as a singleton and reusing it throughout your application. + +Configure the SDK with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split UI, on your Admin settings page, API keys section. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. + + + +```java +import io.split.android.client.SplitClient; +import io.split.android.client.SplitClientConfig; +import io.split.android.client.SplitFactory; +import io.split.android.client.SplitFactoryBuilder; +import io.split.android.client.api.Key; + +// Split SDK key +String sdkKey = "YOUR_SDK_KEY"; + +// Build SDK configuration by default +SplitClientConfig config = SplitClientConfig.builder() + .build(); + +// Create a new user key to be evaluated +// key represents your internal user id, or the account id that +// the user belongs to. +String matchingKey = "key"; +Key key = new Key(matchingKey); + +// Create factory +SplitFactory splitFactory = SplitFactoryBuilder.build(sdkKey, key, config, getApplicationContext()); + +// Get Split Client instance +SplitClient client = splitFactory.client(); +``` + + +```kotlin +import io.split.android.client.SplitClient +import io.split.android.client.SplitClientConfig +import io.split.android.client.SplitFactory +import io.split.android.client.SplitFactoryBuilder +import io.split.android.client.api.Key + +// Split SDK key +val sdkKey = "YOUR_SDK_KEY" + +// Build default SDK configuration +val config: SplitClientConfig = SplitClientConfig.builder().build() + +// Create a new user key to be evaluated +// key represents your internal user id, or the account id that +// the user belongs to. +val matchingKey = "key" +val key: Key = Key(matchingKey) + +// Create factory +val splitFactory: SplitFactory = + SplitFactoryBuilder.build(sdkKey, key, config, applicationContext) + +// Get Split Client instance +val client: SplitClient = splitFactory.client() +``` + + + + +## Using the SDK + +The following explains how to use this SDK. + +### Basic usage + +To make sure the SDK is properly loaded before asking it for a treatment, wait until the SDK is ready as shown below. We set the client to listen for the `SDK_READY` event triggered by the SDK before asking for an evaluation. Once the `SDK_READY` event fires, use the `getTreatment` method to return the proper treatment based on the FEATURE_FLAG_NAME you pass and the key you passed when instantiating the SDK. From there, use an if-else-if block as shown below and plug the code in for the different treatments that you defined in the Split user interface. Make sure to remember the final else branch in your code to handle the client returning control. + + + +```java + client.on(SplitEvent.SDK_READY, new SplitEventTask() { + @Override + public void onPostExecution(SplitClient client) { + // Logic in background here + } + + @Override + public void onPostExecutionView(SplitClient client) { + // Execute logic in main thread here + String treatment = client.getTreatment("FEATURE_FLAG_NAME"); + if (treatment.equals("on")) { + // insert code here to show on treatment + } else if (treatment.equals("off")) { + // insert code here to show off treatment + } else { + // insert your control treatment code here + } + } +}); + +client.on(SplitEvent.SDK_READY_TIMED_OUT, new SplitEventTask() { + @Override + public void onPostExecution(SplitClient client) { + // handle for timeouts here + } + + @Override + public void onPostExecutionView(SplitClient client) { + // handle for timeouts here + } +} + +``` + + +```kotlin + client.on(SplitEvent.SDK_READY, object : SplitEventTask() { + + override fun onPostExecution(client: SplitClient) { + // Execute background logic here + when (client.getTreatment("FEATURE_FLAG_NAME")) { + "on" -> { + // insert code here to show on treatment + } + "off" -> { + // insert code here to show off treatment + } + else -> { + // insert your control treatment code here + } + } + } + + override fun onPostExecutionView(client: SplitClient) { + // Execute main thread logic here + when (client.getTreatment("FEATURE_FLAG_NAME")) { + "on" -> { + // insert code here to show on treatment + } + "off" -> { + // insert code here to show off treatment + } + else -> { + // insert your control treatment code here + } + } + } +}) +``` + + + +Also, a `SDK_READY_FROM_CACHE` event is available, which allows to be aware of when the SDK has loaded data from cache. This way, it is ready to evaluate feature flags using those locally cached definitions. + + + +```java +client.on(SplitEvent.SDK_READY_FROM_CACHE, new SplitEventTask() { + @Override + public void onPostExecution(SplitClient client) { + //Background Code in Here + } + + @Override + public void onPostExecutionView(SplitClient client) { + //UI Code in Here + String treatment = client.getTreatment("FEATURE_FLAG_NAME"); + + if (treatment.equals("on")) { + // insert code here to show on treatment + } else if (treatment.equals("off")) { + // insert code here to show off treatment + } else { + // insert your control treatment code here + } + } + +}); +``` + + +```kotlin +client.on(SplitEvent.SDK_READY_FROM_CACHE, object : SplitEventTask() { + override fun onPostExecution(client: SplitClient) { + // Execute background logic here + } + + override fun onPostExecutionView(client: SplitClient) { + // Execute logic in main thread here + when (client.getTreatment("FEATURE_FLAG_NAME")) { + "on" -> { + // insert code here to show on treatment + } + "off" -> { + // insert code here to show off treatment + } + else -> { + // insert your control treatment code here + } + } + } +}) +``` + + + +### Attribute syntax + +To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK's `getTreatment` methods need to be passed an attribute map at runtime. + +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the Rollout plan as defined in the Split user interface to decide which treatment is assigned to this key. + +The `getTreatment` method supports five types of attributes: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: + +* **Strings:** Use type String. +* **Numbers:** Use type `java.lang.Long` or `java.lang.Integer`. +* **Dates:** Express the value in `milliseconds since epoch`. In Java, `milliseconds since epoch` is of type `java.lang.Long`. For example, the value for the `registered_date` attribute below is `System.currentTimeInMillis()`, which is a long. +* **Booleans:** Use type `java.lang.boolean`. +* **Sets:** Use type `java.util.Collection`. + + + +```java +import java.util.Map; +import java.util.HashMap; +import java.util.Date; + +Map attributes = new HashMap(); +attributes.put("plan_type", "growth"); +attributes.put("registered_date", System.currentTimeMillis()); +attributes.put("deal_size", 1000); +attributes.put("paying_customer", true); +String[] perms = {"read", "write"}; +attributes.put("permissions",perms); + +// See client initialization above +String treatment = client.getTreatment("FEATURE_FLAG_NAME", attributes); + +if (treatment.equals("on")) { + // insert on code here +} else if (treatment.equals("off")) { + // insert off code here +} else { + // insert control code here +} +``` + + +```kotlin + +val attributes = mapOf( + "plan_type" to "growth", + "registered_date" to System.currentTimeMillis(), + "deal_size" to 1000, + "paying_customer" to true, + "permissions" to arrayOf("read", "write") +) + +// See client initialization above +when (client.getTreatment("FEATURE_FLAG_NAME", attributes)) { + "on" -> { + // insert on code here + } + "off" -> { + // insert off code here + } + else -> { + // insert control code here + } +} +``` + + + +### Binding attributes to the client + +Attributes can be bound to the client at any time during the SDK lifecycle. These attributes are stored in memory and used in every evaluation to avoid the need for keeping the attribute set accessible through the whole app. These attributes can be cached into the persistent caching mechanism of the SDK making them available for future sessions, as well as part of the SDK_READY_FROM_CACHE flow by setting the `persistentAttributesEnabled` to true. There is no need to wait for your attributes to be loaded at every session before evaluating flags that use them. + +When an evaluation is called, the attributes provided (if any) at evaluation time are combined with the ones already loaded into the SDK memory, with the ones provided at function execution time take precedence, enabling for those attributes to be overriden/hidden for specific evaluations. + +An attribute is considered valid if it follows one of the types listed below: +- String +- Number +- Boolean +- Array + +The SDK validates these before storing them and if there are invalid or missing values, possibly indicating an issue, the methods return the boolean `false` and do not update any value. + +See below the definitions for the API which is exposed on the `client`: + + + +```java +public interface AttributesManager { + /** + * Set one single attribute and returns true unless there is an issue storing it. + * If `persistentAttributesEnabled` config is enabled, the attribute is also written to the persistent cache. + */ + boolean setAttribute(String attributeName, Object value); + + /** + * Retrieves the value of a given attribute stored in cache. + */ + @Nullable + Object getAttribute(String attributeName); + + /** + * Set multiple attributes and returns true unless there is an issue storing them. + * If `persistentAttributesEnabled` config is enabled, the attributes are also written to the persistent cache. + */ + boolean setAttributes(Map attributes); + + /** + * Retrieves a Map with the values of all attributes stored in cache. + */ + @NonNull + Map getAllAttributes(); + + /** + * Remove one single attribute and returns true unless there is an issue deleting it. + * If `persistentAttributesEnabled` config is enabled, the attribute is also deleted from the persistent cache and won't be available in a subsequent session. + */ + boolean removeAttribute(String attributeName); + + /** + * Clear the whole attribute cache and return true unless there is an issue with the operation and some attributes might still be cached. + * If `persistentAttributesEnabled` config is enabled, the attributes are also deleted from the persistent cache and won't be available in a subsequent session. + */ + boolean clearAttributes(); +} +``` + + +```kotlin +interface AttributesManager { + /** + * Set one single attribute and returns true unless there is an issue storing it. + * If `persistentAttributesEnabled` config is enabled, the attribute is also written to the persistent cache. + */ + fun setAttribute(attributeName: String, value: Any): Boolean + + /** + * Retrieves the value of a given attribute stored in cache. + */ + fun getAttribute(attributeName: String): Any? + + /** + * Set multiple attributes and returns true unless there is an issue storing them. + * If `persistentAttributesEnabled` config is enabled, the attributes are also written to the persistent cache. + */ + fun setAttributes(attributes: Map): Boolean + + /** + * Retrieves a Map with the values of all attributes stored in cache. + */ + fun getAllAttributes(): Map + + /** + * Removes one single attribute and returns true unless there is an issue deleting it. + * If `persistentAttributesEnabled` config is enabled, the attribute is also deleted from the persistent cache and won't be available in a subsequent session. + */ + fun removeAttribute(attributeName: String): Boolean + + /** + * Clears the whole attribute cache and returns true unless there is an issue with the operation and some attributes might still be cached. + * If `persistentAttributesEnabled` config is enabled, the attributes are also deleted from the persistent cache and won't be available in a subsequent session. + */ + fun clearAttributes(): Boolean +} +``` + + + +Refer to the example below to see how to use these methods: + + + +```java +import java.util.Map; +import java.util.HashMap; +import java.util.Date; + + // Prepare a Map with several attributes + Map attributes = new HashMap(); + attributes.put("plan_type", "growth"); + attributes.put("registered_date", System.currentTimeMillis()); + attributes.put("deal_size", 1000); + // Now set these on the client + client.setAttributes(attributes); + + // Set one attribute + boolean result = client.setAttribute("plan_type", "growth"); + + // Get an attribute + Object planType = client.getAttribute("plan_type"); + + // Get all attributes + Map allAttributes = client.getAllAttributes(); + + // Remove an attribute + boolean result = client.removeAttribute("deal_size"); + + // Remove all attributes + boolean result = client.clearAttributes(); +``` + + +```kotlin +// Set multiple attributes +client.setAttributes( + mapOf( + "plan_type" to "growth", + "registered_date" to System.currentTimeMillis(), + "deal_size" to 1000 + ) +) + +// Set one attribute +val result = client.setAttribute("plan_type", "growth") + +// Get an attribute +val planType = client.getAttribute("plan_type") + +// Get all attributes +val allAttributes = client.allAttributes + +// Remove an attribute +val result = client.removeAttribute("deal_size") + +// Remove all attributes +val result = client.clearAttributes() +``` + + + +### Multiple evaluations at once + +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` method of the Split client to do this. +* `getTreatments`: Pass a list of the feature flag names you want treatments for. +* `getTreatmentsByFlagSet`: Evaluate all flags that are part of the provided set name and are cached on the SDK instance. +* `getTreatmentsByFlagSets`: Evaluate all flags that are part of the provided set names and are cached on the SDK instance. + + + +```java +List featureFlagNames = Lists.newArrayList("FEATURE_FLAG_NAME_1", "FEATURE_FLAG_NAME_2"); +Map treatments = client.getTreatments(featureFlagNames, null); + +Map treatmentsByFlagSet = client.getTreatmentsByFlagSet("frontend", null); + +List flagSets = Lists.newArrayList("frontend", "client_side"); +Map treatmentsByFlagSets = client.getTreatmentsByFlagSets(flagSets); + +// Treatments will have the following form: +// { +// "FEATURE_FLAG_NAME_1": "on", +// "FEATURE_FLAG_NAME_2": "visa" +// } + +``` + + +```kotlin +val featureFlagNames: List = listOf("FEATURE_FLAG_NAME_1", "FEATURE_FLAG_NAME_2") +val treatments: Map = client.getTreatments(featureFlagNames, null) + +val treatmentsByFlagSet = client.getTreatmentsByFlagSet("frontend", null) + +val flagSets = listOf("frontend", "client_side") +val treatmentsByFlagSets = client.getTreatmentsByFlagSets(flagSets) + +// Treatments have the following form: +// { +// "FEATURE_FLAG_NAME_1": "on", +// "FEATURE_FLAG_NAME_2": "visa" +// } +``` + + + +### Get treatments with configurations + +To [leverage dynamic configurations with your treatments](https://help.split.io/hc/en-us/articles/360026943552), use the `getTreatmentWithConfig` method. This method returns an object containing the treatment and associated configuration. + +The config element is a stringified version of the configuration JSON defined in the Split user interface. If there is no configuration defined for a treatment, the SDK returns `null` for the config parameter. + +This method takes the exact same set of arguments as the standard `getTreatment` method. See below for examples on proper usage: + + + +```java +SplitResult result = cli.getTreatmentWithConfig("new_boxes", attributes); +Gson gson = new Gson(); +Map map = gson.fromJson(result.config(), Map.class); +String treatment = result.treatment(); +``` + + +```kotlin +val result: SplitResult = client.getTreatmentWithConfig("new_boxes", attributes) +val config: String = result.config() +val treatment: String = result.treatment() +``` + + + +If you need to get multiple evaluations at once, you can also use the `getTreatmentsWithConfig` methods. These methods take the exact same arguments as the [getTreatments](#multiple-evaluations-at-once) methods but return a mapping of feature flag names to SplitResult objects instead of strings. Example usage below. + + + +```java + +List flagNames = Lists.newArrayList("FEATURE_FLAG_NAME_1", "FEATURE_FLAG_NAME_2"); +Map treatments = client.getTreatmentsWithConfig(flagNames, null); + +Map treatmentsByFlagSet = client.getTreatmentsWithConfigByFlagSet("frontend", null); + +List flagSetNames = List.newArrayList("frontend", "client_side"); +Map treatmentsByFlagSets = client.getTreatmentsWithConfigByFlagSets(flagSetNames, null); + +// treatments have the following form: +// { +// "FEATURE_FLAG_NAME_1": { "treatment": "on", "config": "{ \"color\":\"red\" }" }, +// "FEATURE_FLAG_NAME_2": { "treatment": "visa", "config": "{ \"color\":\"red\" }" } +// } +``` + + +```kotlin +val flagNames: List = listOf("FEATURE_FLAG_NAME_1", "FEATURE_FLAG_NAME_2") +val treatments: Map = client.getTreatmentsWithConfig(flagNames, null) + +val treatmentsByFlagSet = client.getTreatmentsWithConfigByFlagSet("frontend", null) + +val flagSetNames = listOf("frontend", "client_side"); +val treatmentsByFlagSets = client.getTreatmentsWithConfigByFlagSets(flagSetNames, null) + +// treatments have the following form: +// { +// "FEATURE_FLAG_NAME_1": { "treatment": "on", "config": "{ \"color\":\"red\" }" }, +// "FEATURE_FLAG_NAME_2": { "treatment": "visa", "config": "{ \"color\":\"red\" }" } +// } +``` + + + +### Shutdown + +It is good practice to call the `destroy` method before your app shuts down or is destroyed, as this method gracefully shuts down the Split SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. + + + +```java +client.destroy(); +``` + + +```kotlin +client.destroy() +``` + + + +After `destroy()` is called, any subsequent invocations to the `client.getTreatment()` or `manager` methods result in `control` or empty list respectively. + +:::warning[Important!] +A call to the `destroy()` method also destroys the factory object. When creating new client instance, first create a new factory instance. +::: + +## Track + +Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users' actions and metrics. Refer to the [Events](https://help.split.io/hc/en-us/articles/360020585772) guide to learn about using track events in Split. In the examples below, you can see that the `.track()` method can take up to four arguments. The proper data type and syntax for each are: + +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: + * Contains 80 characters or fewer. + * Starts with a letter or number. + * Contains only letters, numbers, hyphen, underscore, or period. + * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}` +* **VALUE:** (Optional) The value to be used in creating the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is **Float**. +* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. + +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event to be sent back to Split's servers on the next event post. The SDK will return `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method has been provided. + +In case a bad input is provided, refer to the [Track events](https://help.split.io/hc/en-us/articles/360020585772-Track-events) guide for information about our SDK's expected behavior. + + + +```java +// Event without a value +boolean trackEvent = client.track("TRAFFIC_TYPE", "EVENT_TYPE"); +// Example +boolean trackEvent = client.track("user", "page_load_time"); + +// Associate value to an event +boolean trackEvent = client.track("TRAFFIC_TYPE", "EVENT_TYPE", VALUE); +// Example +boolean trackEvent = client.track("user", "page_load_time", 83.334); + +// If you would like to send an event but you've already defined the traffic type in the config of the client +boolean trackEvent = client.track("EVENT_TYPE"); +// Example +boolean trackEvent = client.track("page_load_time"); + +// If you would like to associate a value to an event and you've already defined the traffic type in the config of the client +boolean trackEvent = client.track("EVENT_TYPE", VALUE); +// Example +boolean trackEvent = client.track("page_load_time", 83.334); + +// If you would like to associate a value and properties to an event +boolean trackEvent = client.track("key", "TRAFFIC_TYPE", "EVENT_TYPE", VALUE, {PROPERTIES}); +// Example +HashMap properties = new HashMap<>(); +properties.put("package", "premium"); +properties.put("admin", true); +properties.put("discount", 50); + +boolean trackEvent = client.track("john@doe.com", "user", "page_load_time", 83.334, properties); + +// If you would like to associate just properties to an event +boolean trackEvent = client.track("key", "TRAFFIC_TYPE", "EVENT_TYPE", null, {PROPERTIES}); +// Example +HashMap properties = new HashMap<>(); +properties.put("package", "premium"); +properties.put("admin", true); +properties.put("discount", 50); + +boolean trackEvent = client.track("john@doe.com", "user", "page_load_time", null, properties); +``` + + +```kotlin +// Event without a value +val trackEvent = client.track("user", "page_load_time") + +// Associate value to an event +val trackEvent = client.track("user", "page_load_time", 83.334) + +// If you would like to send an event but you've already defined the traffic type in the config of the client +val trackEvent = client.track("page_load_time") + +// If you would like to associate a value to an event and you've already defined the traffic type in the config of the client +val trackEvent = client.track("page_load_time", 83.334) + +// If you would like to associate a value and properties to an event +val trackEvent = client.track( + "john@doe.com", + "user", + "page_load_time", + 83.334, + mapOf( + "package" to "premium", + "admin" to true, + "discount" to 50 + ) +) + +// If you would like to associate just properties to an event +val trackEvent = client.track( + "john@doe.com", + "user", + "page_load_time", + null, + mapOf( + "package" to "premium", + "admin" to true, + "discount" to 50 + ) +) +``` + + + +## Configuration + +The SDK has a number of knobs for configuring performance. Each knob is tuned to a reasonable default. However, you can override the value while instantiating the SDK. The parameters available for configuration are shown below. + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| featuresRefreshRate | The SDK polls Split servers for changes to feature flags at this rate (in seconds). | 3600 seconds | +| segmentsRefreshRate | The SDK polls Split servers for changes to segments at this rate (in seconds). | 1800 seconds | +| impressionsRefreshRate | The treatment log captures which customer saw what treatment (on, off, etc.) at what time. This log is periodically flushed back to Split servers. This configuration controls how quickly the cache expires after a write (in seconds). | 1800 seconds | +| telemetryRefreshRate | The SDK caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers (in seconds). | 3600 seconds (1 hour) | +| eventsQueueSize | When using `.track`, the number of **events** to be kept in memory. | 10000 | +| eventFlushInterval | When using `.track`, how often is the events queue flushed to Split's servers. | 1800 seconds | +| eventsPerPush | Maximum size of the batch to push events. | 2000 | +| trafficType | When using `.track`, the default traffic type to be used. | not set | +| connectionTimeout | HTTP client connection timeout (in ms). | 10000 ms | +| readTimeout | HTTP socket read timeout (in ms). | 10000 ms | +| impressionsQueueSize | Default queue size for impressions. | 30K | +| disableLabels | Disable labels from being sent to Split backend. Labels may contain sensitive information. | true | +| logLevel | Enables logging according to the level specified. Options are `NONE`, `VERBOSE`, `DEBUG`, `INFO`, `WARNING`, `ERROR`, and `ASSERT`. | `NONE` | +| proxyHost | The location of the proxy using standard URI: `scheme://user:password@domain:port/path`. If no port is provided, the SDK defaults to port 80. | null | +| ready | Maximum amount of time in milliseconds to wait before notifying a timeout. | -1 (not set) | +| synchronizeInBackground | Activates synchronization when application host is in background. | false | +| synchronizeInBackgroundPeriod | Rate in minutes in which the background synchronization would check the conditions and trigger the data fetch if those are met. Minimum rate allowed is 15 minutes. | 15 | +| backgroundSyncWhenBatteryNotLow | When set to true, synchronize in background only if battery level is not low. | true | +| backgroundSyncWhenWifiOnly | When set to true, synchronize in background only when the available connection is wifi (unmetered). When false, background synchronization takes place as long as there is an available connection. | false | +| streamingEnabled | Boolean flag to enable the streaming service as default synchronization mechanism when in foreground. In the event of an issue with streaming, the SDK will fallback to the polling mechanism. If false, the SDK will poll for changes as usual without attempting to use streaming. | true | +| syncConfig | Optional SyncConfig instance. Use it to filter specific feature flags to be synced and evaluated by the SDK. These filters can be created with the `SplitFilter::bySet` static function (recommended, flag sets are available in all tiers), or `SplitFilter::byName` static function, and appended to this config using the `SyncConfig` builder. If not set or empty, all feature flags are downloaded by the SDK. | null | +| persistentAttributesEnabled | Enables saving attributes on persistent cache which is loaded as part of the SDK_READY_FROM_CACHE flow. All functions that mutate the stored attributes map affect the persistent cache. | false | +| syncEnabled | Controls the SDK continuous synchronization flags. When `true`, a running SDK processes rollout plan updates performed in the Split user interface (default). When `false`, it fetches all data upon init, which ensures a consistent experience during a user session and optimizes resources when these updates are not consumed by the app. | true | +| impressionsMode | This configuration defines how impressions (decisioning events) are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Split; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, ALL impressions are queued and sent to Split; this is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. | `OPTIMIZED` | +| userConsent | User consent status used to control the tracking of events and impressions. Possible values are `GRANTED`, `DECLINED`, and `UNKNOWN`. See [User consent](#user-consent) for details. | `GRANTED` | +| encryptionEnabled | If set to `true`, the local database contents is encrypted. | false | +| prefix | If set, the prefix will be prepended to the database name used by the SDK. | null | +| certificatePinningConfiguration | If set, enables certificate pinning for the given domains. For details, see the [Certificate pinning](#certificate-pinning) section below. | null | + +To set each of the parameters defined above, use the syntax below. + + + +```java +import io.split.android.client.*; +import java.util.Arrays; + +SplitFilter splitFilter = SplitFilter.bySet(Arrays.asList("frontend")); + +SplitClientConfig config = SplitClientConfig.builder() + .impressionsRefreshRate(60) + .connectionTimeout(15000) + .readTimeout(15000) + .syncConfig(SyncConfig.builder() + .addSplitFilter(splitFilter) + .build()) + .build(); + +// Split SDK key +String sdkKey = "YOUR_SDK_KEY"; + +// Create a new user key to be evaluated +String matching = "key"; +String bucketingKey = null; +Key k = new Key(matchingKey,bucketingKey); + +// Create factory +SplitFactory splitFactory = SplitFactoryBuilder.build(sdkKey, k, config, getApplicationContext()); + +// Get Split Client instance +SplitClient client = splitFactory.client(); +``` + + +```kotlin +val splitFilter: SplitFilter = SplitFilter.bySet(listOf("frontend")) + +val config: SplitClientConfig = SplitClientConfig.builder() + .impressionsRefreshRate(60) + .connectionTimeout(15000) + .readTimeout(15000) + .syncConfig( + SyncConfig.builder() + .addSplitFilter(splitFilter) + .build() + ) + .build() + +val splitFactory: SplitFactory = SplitFactoryBuilder + .build( + "YOUR_SDK_KEY", + Key("key"), + config, + applicationContext + ) + +val client: SplitClient = splitFactory.client() +``` + + + +## Localhost mode + +For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, you can start the Split SDK in **localhost** mode (aka, off-the-grid mode). In this mode, the SDK neither polls or updates Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the feature flags. To use the SDK in localhost mode, replace the API Key with `localhost`, as shown in the example below: + +Since version 2.2.0, our SDK supports a new type of localhost feature flag definition file, using the YAML format. This new format allows the user to map different keys to different treatments within a single feature flag, and also add configurations to them. The new format is a list of single-key maps (one per mapping feature_flag-keys-config), defined as follows: + +```yaml title="YAML" +## - feature_name: +## treatment: "treatment_applied_to_this_entry" +## keys: "single_key_or_list" +## config: "{\"desc\" : \"this applies only to ON treatment\"}" + +- my_feature: + treatment: "on" + keys: "key" + config: "{\"desc\" : \"this applies only to ON treatment\"}" +- some_other_feature: + treatment: "off" +- my_feature: + treatment: "off" +- other_feature: + treatment: "off" + keys: ["key_1", "key_2"] + config: "{\"desc\" : \"this overrides multiple keys and returns off treatment for those keys\"}" +``` + +In the example above, we have four entries: + + * The first entry defines that for feature flag `my_feature`, the key `key` returns the treatment `on` and the `on` treatment is tied to the configuration `{"desc" : "this applies only to ON treatment"}`. + * The second entry defines that the feature flag `some_other_feature` always returns the `off` treatment and no configuration. + * The third entry defines that `my_feature` always returns `off` for all keys that don't match another entry (in this case, any key other than `key`). + * The fourth entry shows an example on how to override a treatment for a set of keys. + +In this mode, the SDK loads the yaml file from a resource bundle file at the assets' project `src/main/assets/splits.yaml`. + + + +```java +import io.split.android.client.SplitClient; +import io.split.android.client.SplitFactoryBuilder; +import io.split.android.client.api.Key; + +// Create a new user key to be evaluated +String matching = "key"; +Key k = new Key(matchingKey); + +SplitClient client = SplitFactoryBuilder.build("localhost", k, getApplicationContext()).client(); +``` + + +```kotlin +import io.split.android.client.SplitFactoryBuilder +import io.split.android.client.api.Key + +// Create a new user key to be evaluated +val key = Key("key") + +val client = SplitFactoryBuilder.build( + "localhost", + key, + applicationContext +).client() +``` + + + +If a split.yaml or split.yml is not found in assets, Split SDK maintains backward compatibility by trying to load the legacy file (split.properties), which is now deprecated. + +The format of this file is a properties file as key-value line. The key is the feature flag name, and the value is the treatment name. The following is a sample `split.properties` file: + +```java title="split.properties" +## this is a comment + +## sdk.getTreatment(*, reporting_v2) will return 'on' +reporting_v2=on + +double_writes_to_cassandra=off + +new-navigation=v3 +``` + +## Manager + +Use the Split Manager to get a list of feature flags available to the Split client. To instantiate a Manager in your code base, use the same factory that you used for your client. + + + +```java +SplitFactory splitFactory = SplitFactoryBuilder.build("YOUR_API_KEY"); +SplitManager manager = splitFactory.manager(); +``` + + +```kotlin +val splitFactory: SplitFactory = SplitFactoryBuilder.build( + "api_key", + Key("key"), + applicationContext +) + +val manager: SplitManager = splitFactory.manager() +``` + + + +The Manager then has the following methods available. + + + +```java +/** + * Retrieves the feature flags that are currently registered with the + * SDK. + * + * @return a List of SplitView or empty. + */ +List splits(); + +/** + * Returns the feature flags registered with the SDK of this name. + * + * @return SplitView or null + */ +SplitView split(String SplitName); + +/** + * Returns the names of feature flags registered with the SDK. + * + * @return a List of String (Split feature names) or empty + */ +List splitNames(); +``` + + +```kotlin +/** + * Retrieves the feature flags that are currently registered with the + * SDK. + * + * @return a List of SplitView or empty. + */ +fun splits(): List + +/** + * Returns the feature flags registered with the SDK of this name. + * + * @return SplitView or null + */ +fun split(SplitName: String): SplitView? + +/** + * Returns the names of features flags registered with the SDK. + * + * @return a List of String (feature flag names) or empty + */ +fun splitNames(): List +``` + + + +The `SplitView` object referenced above has the following structure. + + + +```java +public class SplitView { + public String name; + public String trafficType; + public boolean killed; + public List treatments; + public long changeNumber; + public Map configs; + public String defaultTreatment; + public List sets; + public boolean impressionsDisabled; +} +``` + + +```kotlin +class SplitView( + var name: String?, + var trafficType: String?, + var killed: Boolean, + var treatments: List?, + var changeNumber: Long + var defaultTreatment: String? + var sets: List + var impressionsDisabled: Boolean +) +``` + + + +## Listener + +Split SDKs send impression data back to Split servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression listener*. + +The SDK sends the generated impressions to the impression listener right away. Because of this, be careful while implementing handling logic to avoid blocking the main thread. Generally speaking, you should create a separate thread to handle incoming impressions. Refer to the snippet below: + + + +```java +SplitClientConfig config = SplitClientConfig.builder() + .impressionListener(new MyImpressionListener()) + .build(); + +class MyImpressionListener implements ImpressionListener { + @Override + public void log(Impression impression) { + // Do something on UI thread + new Thread(new Runnable() { + public void run() { + // Do something in another thread (use this most of the time!) + } + }).start(); + } + + @Override + public void close() { + } +} +``` + + +```kotlin +class MyImpressionListener : ImpressionListener { + override fun log(impression: Impression) { + // Do something on UI thread + Thread { + // Do something in another thread (use this most of the time!) + }.start() + } + + override fun close() { + + } +} + +val config = SplitClientConfig.builder() + .impressionListener(MyImpressionListener()) + .build() +``` + + + +In regards with the data available here, refer to the `Impression` objects interface and information about each field below: + + + +```java + String key(); + String bucketingKey(); + String split(); + String treatment(); + Long time(); + String appliedRule(); + Long changeNumber(); + Map attributes(); + Long previousTime(); +``` + + +```kotlin + key(): String? + bucketingKey(): String? + split(): String? + treatment(): String? + time(): Long? + appliedRule(): String? + changeNumber(): Long? + attributes(): Map? + previousTime(): Long? +``` + + + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| key | String | Key which is evaluated. | +| bucketingKey | String | Key which is used for bucketing, if provided. | +| split | String | Feature flag which is evaluated. | +| treatment | String | Treatment that is returned. | +| time | Long | Timestamp of when the impression is generated. | +| appliedRule | String | Targeting rule in the definition that matched resulting in the treatment being returned. | +| changeNumber | Long | Date and time of the last change to the targeting rule that the SDK used when it served the treatment. It is important to understand when a change made to a feature flag is picked up by the SDKs and whether one of the SDK instances is not picking up changes. | +| attributes | Map\ | A map of attributes passed to `getTreatment`/`getTreatments`, if any. | +| previousTime | Long | If SDK is deduping and a matching impression is seen before on the lifetime of the instance this is its timestamp. | + +## Flush + +The flush() method sends the data stored in memory (impressions and events) to the Split cloud and clears the successfully posted data. If a connection issue is experienced, the data is sent on the next attempt. If you want to flush all pending data when your app goes to background, a good place to call this method is the onPause callback of your MainActivity. + + + +```java +client.flush(); +``` + + +```kotlin +client.flush() +``` + + + +## Logging + +To enable SDK logging, the `logLevel` setting is available in `SplitClientConfig` class: + +```swift title="Setup logs" +// This setting type is `SplitLogLevel`. +// The available values are DEBUG, INFO, WARNING, ERROR, ASSERT and NONE +SplitClientConfig.Builder builder = SplitClientConfig.builder() + .logLevel(Log.VERBOSE) +SplitClientConfig config = builder.build(); + + ... +``` + +The following shows an example output: + +

+ android_log_example.png +

+ +## Advanced use cases + +This section describes advanced use cases and features provided by the SDK. + +### Instantiate multiple SDK clients + +In versions previous to 2.10.0, you had to create more that one SDK instance to evaluate for different users IDs. From 2.10.0 on, Split supports the ability to create multiple clients, one for each user ID. For example, if you need to roll out feature flags for different user IDs, you can instantiate multiple clients, one for each ID. You can then evaluate them using the corresponding client. + +You can do this using the example below: + + + +```java +// Create factory +Key key = new Key("anonymous_user"); +SplitClientConfig config = SplitClientConfig.builder().build(); +SplitFactory splitFactory = SplitFactoryBuilder.build("yourAuthKey", key, config, getApplicationContext()); + +// Now when you call factory.client(), the SDK will create a client +// using the Key you passed in during the factory creation +SplitClient anonymousClient = splitFactory.client(); + +// To create another client for a user instead, just pass in a different Key or id +SplitClient userClient = splitFactory.client("user_id"); + +// Add events handler for each client to be notified when SDK is ready +anonymousClient.on(SplitEvent.SDK_READY, new SplitEventTask() { + @Override + public void onPostExecutionView(SplitClient client) { + // Check treatment for account-permissioning and anonymousClient + String accountPermissioningTreatment = anonymousClient.getTreatment("account-permissioning"); + } +}); + +userClient.on(SplitEvent.SDK_READY, new SplitEventTask() { + @Override + public void onPostExecutionView(SplitClient client) { + // Check treatment for user-poll and userClient + String userPollTreatment = userClient.getTreatment("user-poll"); + } +}); +``` + + +```kotlin +// Create factory +val key = Key("anonymous_user") +val config = SplitClientConfig.builder().build() +val splitFactory = SplitFactoryBuilder.build("yourAuthKey", key, config, getApplicationContext()) + +// Now when you call factory.client(), the SDK will create a client +// using the Key you passed in during the factory creation +val anonymousClient = splitFactory.client() + +// To create another client for a user instead, just pass in a different Key or id +val userClient = splitFactory.client("user_id") + +// Add events handler for each client to be notified when SDK is ready +anonymousClient.on(SplitEvent.SDK_READY, object : SplitEventTask() { + override fun onPostExecutionView(client: SplitClient) { + // Check treatment for account-permissioning and anonymousClient + val accountPermissioningTreatment = anonymousClient.getTreatment("account-permissioning") + } +}) + +userClient.on(SplitEvent.SDK_READY, object : SplitEventTask() { + override fun onPostExecutionView(client: SplitClient) { + // Check treatment for user-poll and userClient + val userPollTreatment = userClient.getTreatment("user-poll") + } +}) +``` + + + +:::info[Number of SDK instances] +While the SDK does not put any limitations on the number of instances that can be created, we strongly recommend keeping the number of SDKs down to **one** or **two**. +::: + +### Subscribe to events + +You can listen for four different events from the SDK. + +* `SDK_READY_FROM_CACHE`. This event fires once the SDK is ready to evaluate treatments using a locally cached version of your rollout plan from a previous session (which might be stale). If there is data in the cache, this event fires almost immediately, since access to the cache is fast; otherwise, it doesn't fire. +* `SDK_READY`. This event fires once the SDK is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Split servers. +* `SDK_READY_TIMED_OUT `. This event fires if there is no cached version of your rollout plan in disk cache, and the SDK could not fully download the data from Split servers within the time specified by the `ready` setting of the `SplitClientConfig` object. This event does not indicate that the SDK initialization was interrupted. The SDK continues downloading the rollout plan and fires the `SDK_READY` event when finished. This delayed `SDK_READY` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. +* `SDK_UPDATE`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in the Split user interface. + +An event is an extension of a SplitEventTask. + + + +```java +public class SplitEventTask { + public void onPostExecution(SplitClient client) { } + public void onPostExecutionView(SplitClient client) { } +} +``` + + +```kotlin +open class SplitEventTask { + + open fun onPostExecution(client: SplitClient) { + + } + + open fun onPostExecutionView(client: SplitClient) { + + } +} +``` + + + +`onPostExecution` is executed in the background when the event is triggered. This step is used to perform background computation, which can take a long time. + +`onPostExecutionView` is invoked on the UI thread after `onPostExecution` finishes. + +The syntax to listen for an event can be seen below. + + + +```java +client.on(SplitEvent.SDK_READY, new SplitEventTask() { + @Override + public void onPostExecution(SplitClient client) { + // Background Code in Here + } + + @Override + public void onPostExecutionView(SplitClient client) { + // UI Code in Here + } +}); + +// When definitions and any bound attributes were loaded from cache +client.on(SplitEvent.SDK_READY_FROM_CACHE, new SplitEventTask() { + @Override + public void onPostExecution(SplitClient client) { + // Background Code in Here + } + + @Override + public void onPostExecutionView(SplitClient client) { + // UI Code in Here + } +}); + +// When the SDK couldn't fetch definitions before *config.ready* time +client.on(SplitEvent.SDK_READY_TIMED_OUT, new SplitEventTask() { + @Override + public void onPostExecution(SplitClient client) { + // Background Code in Here + } + + @Override + public void onPostExecutionView(SplitClient client) { + // UI Code in Here + } +}); + +// When definitions have changed +client.on(SplitEvent.SDK_READY_UPDATE, new SplitEventTask() { + @Override + public void onPostExecution(SplitClient client) { + // Background Code in Here + } + + @Override + public void onPostExecutionView(SplitClient client) { + // UI Code in Here + } +}); +``` + + +```kotlin +client.on(SplitEvent.SDK_READY, object : SplitEventTask() { + override fun onPostExecution(client: SplitClient) { + // Background Code in Here + } + + override fun onPostExecutionView(client: SplitClient) { + // UI Code in Here + } +}) + +// When definitions were loaded from cache +client.on(SplitEvent.SDK_READY_FROM_CACHE, object : SplitEventTask() { + override fun onPostExecution(client: SplitClient) { + // Background Code in Here + } + + override fun onPostExecutionView(client: SplitClient) { + // UI Code in Here + } +}) + +// When the SDK couldn't fetch definitions before *config.ready* time +client.on(SplitEvent.SDK_READY_TIMED_OUT, object : SplitEventTask() { + override fun onPostExecution(client: SplitClient) { + // Background Code in Here + } + + override fun onPostExecutionView(client: SplitClient) { + // UI Code in Here + } +}) + +// When definitions have changed +client.on(SplitEvent.SDK_UPDATE, object : SplitEventTask() { + override fun onPostExecution(client: SplitClient) { + // Background Code in Here + } + + override fun onPostExecutionView(client: SplitClient) { + // UI Code in Here + } +}) +``` + + + +### User consent + +The SDK allows you to disable the tracking of events and impressions until user consent is explicitly granted or declined. + +The `userConsent` configuration parameter lets you set the initial consent status of the SDK instance, and the factory method `setUserConsent(enabled: Bool)` lets you grant (enable) or decline (disable) dynamic data tracking. + +There are three possible initial states: + * `'GRANTED'`: The user grants consent for tracking events and impressions. The SDK sends them to Split cloud. This is the default value if `userConsent` param is not defined. + * `'DECLINED'`: The user declines consent for tracking events and impressions. The SDK does not send them to Split cloud. + * `'UNKNOWN'`: The user neither grants nor declines consent for tracking events and impressions. The SDK tracks them in its internal storage, and eventually either sends them or not if the consent status is updated to `'GRANTED'` or `'DECLINED'` respectively. + +The status can be updated at any time with the `setUserConsent` factory method. + +Working with user consent is demonstrated below. + +```java title="User consent: Initial config, getter and setter" +// Overwrites the initial consent status of the factory instance, which is 'GRANTED' by default. +// 'UNKNOWN' status represents that the user has neither granted nor declined consent for tracking data, +// so the SDK locally tracks data but not send it to Split cloud until consent is changed to 'GRANTED'. +SplitClientConfig config = SplitClientConfig.builder() + .userConsent(UserConsent.UNKNOWN) + .build(); + +try { + SplitFactory factory = SplitFactoryBuilder.build("YOUR_SDK_KEY", + new Key(mUserKey, null), + config, context); + + // Changed User Consent status to 'GRANTED'. Data is sent to Split cloud. + factory.setUserConsent(true); + // Changed User Consent status to 'DECLINED'. Data is not sent to Split cloud. + factory.setUserConsent(false); + + // The 'getUserConsent' method returns User Consent status. + // We expose the constants for customer checks and tracking. + if (factory.getUserConsent() == UserConsent.DECLINED) { + Log.i(TAG, "USER CONSENT DECLINED"); + } + if (factory.getUserConsent() == UserConsent.GRANTED) { + Log.i(TAG, "USER CONSENT GRANTED"); + } + if (factory.getUserConsent() == UserConsent.UNKNOWN) { + Log.i(TAG, "USER CONSENT UNKNOWN"); + } + +} catch (Exception e) { +} +``` + +### Certificate pinning + +The SDK allows you to constrain the certificates that the SDK trusts, using one of the following techniques: + +1. Pin a certificate's `SubjectPublicKeyInfo`, by providing the public key as a ___base64 SHA-256___ hash or a ___base64 SHA-1___ hash. +2. Pin a certificate's entire certificate chain (the root, all intermediate, and the leaf certificate), by providing the certificate chain as a .der file. + +Each pin corresponds to a host. For subdomains, you can optionally use wildcards, where `*` will match one subdomain (e.g. `*.example.com`), and `**` will match any number of subdomains (e.g `**.example.com`). + +You can optionally configure a listener to execute on certificate validation failure for a host. + +To set the SDK to require pinned certificates for specific hosts, add the `CertificatePinningConfiguration` object to `SplitClientConfig.Builder`, as shown below. + + + +```java +import io.split.android.client.network.CertificatePinningConfiguration; +import io.split.android.client.SplitClientConfig; +import com.yourApp.R; // to reference your res/ folder + +// Define pins for certificate pinning +CertificatePinningConfiguration certPinningConfig = CertificatePinningConfiguration.builder() + + // Provide a base 64 SHA-256 hash + .addPin("*.example1.com", "sha256/7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=") + + // Provide a certificate chain as a 'res/raw/cert.der' file + .addPin("*.example2.com", context.getResources().openRawResource(R.raw.cert)) + + // Provide a listener to log failure + .failureListener((host, certificateChain) -> { + Log.d("CertPinning", "Certificate pinning failure for " + host); + }) + + .build(); + +// Set the CertificatePinningConfiguration property for the Split client configuration +SplitClientConfig config = SplitClientConfig.builder() + .certificatePinningConfiguration(certPinningConfig) + // you can add other configuration properties here + .build(); +``` + + +```kotlin +import io.split.android.client.network.CertificatePinningConfiguration +import io.split.android.client.SplitClientConfig +import com.yourApp.R // to reference your res/ folder + +// Define pins for certificate pinning +val certPinningConfig = CertificatePinningConfiguration.builder() + + // Provide a base 64 SHA-256 hash + .addPin("*.example1.com", "sha256/7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=") + + // Provide a certificate chain as a 'res/raw/cert.der' file + .addPin("*.example2.com", context.getResources().openRawResource(R.raw.cert)) + + // Provide a listener to log failure + .failureListener { host, certificateChain -> + Log.d("CertPinning", "Certificate pinning failure for $host") + } + + .build() + +// Set the CertificatePinningConfiguration property for the Split client configuration +val config = SplitClientConfig.builder() + .certificatePinningConfiguration(certPinningConfig) + // you can add other configuration properties here + .build() + +``` + + \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/angular-utilities.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/angular-utilities.md new file mode 100644 index 00000000000..9cfd029e3c1 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/angular-utilities.md @@ -0,0 +1,569 @@ +--- +title: Angular utilities +sidebar_label: Angular utilities +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +This guide provides detailed information about our Angular utilities built on top of our [JavaScript Browser SDK](https://help.split.io/hc/en-us/articles/360058730852-Browser-SDK). An Angular Service and an Angular Guard are provided in this utilities in ESM2020, FESM2020 and FESM2015 module formats. The service provides an easy way to interact with the underneath SDK and work towards any use cases through simplified methods. You can also import from this utilities an Angular Guard to wait for SDK to be ready. + +All of our SDKs are open source. Go to our [Angular Utilities GitHub repository](https://github.com/splitio/angular-sdk-plugin) to see the source code. + +## Language support + +These utilities guarantee support with Angular v15.2.10 or later. + +## Initialization + +Set up Split in your code base with the following two steps: + +### 1. Import the utilities into your project + +Import the utilities into your project using the following NPM command: + +```bash title="NPM" +npm install --save @splitsoftware/splitio-angular@3.0.0 +``` + +### 2. Instantiate the service + +```javascript title="TypeScript (using ES modules)" +import { SplitService } from '@splitsoftware/splitio-angular'; + +const sdkReady = false; + +// Inject service +constructor(private splitService: SplitService){} + +// Instantiate the Service +public initPlugin() { + + // Create the config for the plugin. + const sdkConfig = { + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + } + }; + + // init method returns an observable for sdk readiness + this.splitService.init(sdkConfig).subscribe(() => { + this.sdkReady = true + }); +} +``` + +:::info[Notice for TypeScript] +With the SDK package on NPM, you get the SplitIO namespace, which contains useful types and interfaces for you to use. + +Feel free to access the declaration files if IntelliSense is not enough. +::: + +We recommend instantiating the service once as a singleton and reusing it throughout your application. + +Configure the service with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split UI, on your Admin settings page, API keys section. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. + +## Using the service + +### Basic use + +When the SDK is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while its in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). + +To make sure the SDK is properly loaded before asking it for a treatment, block until the SDK is ready, as shown below. You can subscribe to `splitService.sdkReady$` observable provided by splitService before asking for an evaluation. + +After the observable calls back, you can use the `getTreatment` method to return the proper treatment based on the `FEATURE_FLAG_NAME` and the `key` variable you passed when instantiating the SDK. + +Then use an if-else-if block as shown below and insert the code for the different treatments that you defined in the Split user interface. Remember the final else branch in your code to handle the client returning control. + +```javascript title="TypeScript" +this.splitService.sdkReady$.subscribe(() => { + const treatment: SplitIO.Treatment = this.splitService.getTreatment('FEATURE_FLAG_NAME'); + + if (treatment == "on") { + // insert code here to show on treatment + } else if (treatment == "off") { + // insert code here to show off treatment + } else { + // insert your control treatment code here + } +}); +``` + +### Attribute syntax + +To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the splitService's `getTreatment` method needs to be passed an attribute map at runtime. + +In the example below, we are rolling out a feature to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in the Split user interface to decide whether to show the `on` or `off` treatment to this account. + +The `getTreatment` method supports five types of attributes: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: + +* **Strings:** Use type String. +* **Numbers:** Use type Number. +* **Dates:** Use type Date and express the value in `milliseconds since epoch`.
*Note:* Milliseconds since epoch is expressed in UTC. If your date or date-time combination is in a different timezone, first convert it to UTC, then transform it to milliseconds since epoch. +* **Booleans:** Use type Boolean. +* **Sets:** Use type Array. + +```javascript title="TypeScript" +const attributes: SplitIO.Attributes = { + // date attributes are handled as `millis since epoch` + registered_date: new Date('YYYY-MM-DDTHH:mm:ss.sssZ').getTime(), + // this string will be compared against a list called `plan_type` or against another string + plan_type: 'growth', + // this number will be compared agains a number value called `deal_size` + deal_size: 10000, + // this array will be compared against a set called `permissions` + permissions: ['read', 'write'] +}; + +const treatment: SplitIO.Treatment = this.splitService.getTreatment('FEATURE_FLAG_NAME', attributes); + +if (treatment === 'on') { + // insert on code here +} else if (treatment === 'off') { + // insert off code here +} else { + // insert control code here +} +``` + +You can pass your attributes in exactly this way to the `splitService.getTreatments` method. + +### Multiple evaluations at once + +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` from the Split client to do this. +* `getTreatments`: Pass a list of the feature flag names you want treatments for. +* `getTreatmentsByFlagSet`: Evaluate all flags that are part of the provided set name and are cached on the SDK instance. +* `getTreatmentsByFlagSets`: Evaluates all flags that are part of the provided set names and are cached on the SDK instance. + + + +```javascript +const flagNames = ['FEATURE_FLAG_NAME_1', 'FEATURE_FLAG_NAME_2']; + +const treatments: SplitIO.Treatments = this.splitService.getTreatments(flagNames); + +// treatments will have the following form: +// { +// FEATURE_FLAG_NAME_1: 'on', +// FEATURE_FLAG_NAME_2: 'visa' +// } +``` + + +```javascript + +const treatments: SplitIO.Treatments = this.splitService.getTreatmentsByFlagSet('frontend'); + +// treatments will have the following form: +// { +// FEATURE_FLAG_NAME_1: 'on', +// FEATURE_FLAG_NAME_2: 'visa' +// } +``` + + +```javascript +const flagSetNames = ['frontend', 'client_side']; + +const treatments: SplitIO.Treatments = this.splitService.getTreatmentsByFlagSets(flagSetNames); + +// treatments will have the following form: +// { +// FEATURE_FLAG_NAME_1: 'on', +// FEATURE_FLAG_NAME_2: 'visa' +// } +``` + + + +### Get treatments with configurations + +To [leverage dynamic configurations with your treatments](https://help.split.io/hc/en-us/articles/360026943552), use the `getTreatmentWithConfig` method. This method returns an object with the structure below: + +```javascript title="TypeScript" +type TreatmentResult = { + treatment: string, + config: string | null +}; +``` + +From the object structure, the config is a stringified version of the configuration JSON defined in the Split user interface. If no configuration is defined for a treatment, the SDK returns `null` for the config parameter. This method takes the same set of arguments as the standard `getTreatment` method. Refer to the examples below for proper usage: + +```javascript title="TypeScript" +const treatmentResult: SplitIO.TreatmentWithConfig = this.splitService.getTreatmentWithConfig('FEATURE_FLAG_NAME', attributes); +const configs = JSON.parse(treatmentResult.config); +const treatment = treatmentResult.treatment; + +if (treatment === 'on') { + // insert on code here and use configs here as necessary +} else if (treatment === 'off') { + // insert off code here and use configs here as necessary +} else { + // insert control code here +} +``` + +If you need to get multiple evaluations at once, you can also use the `getTreatmentsWithConfig` methods. These methods take the exact same arguments as the [getTreatments](#multiple-evaluations-at-once) methods but return a mapping of feature flag names to TreatmentResults objects instead of strings. Example usage below. + + + +```javascript +const featureFlagNames = ['FEATURE_FLAG_NAME_1', 'FEATURE_FLAG_NAME_2']; + +const treatmentResults: SplitIO.TreatmentsWithConfig = this.splitService.getTreatmentsWithConfig(featureFlagNames); + +// treatmentResults will have the following form: +// { +// FEATURE_FLAG_NAME_1: {treatment: 'on', +// config: "{ 'color' : 'red'}}", +// FEATURE_FLAG_NAME_2: {treatment: 'v2', +// config: "{ 'copy' : 'better copy'}}", +// } +``` + + +```javascript +const treatmentResults: SplitIO.TreatmentsWithConfig = this.splitService.getTreatmentsWithConfigByFlagSet('frontend'); + +// treatmentResults will have the following form: +// { +// FEATURE_FLAG_NAME_1: {treatment: 'on', +// config: "{ 'color' : 'red'}}", +// FEATURE_FLAG_NAME_2: {treatment: 'v2', +// config: "{ 'copy' : 'better copy'}}", +// } +``` + + +```javascript +const flagSetsNames = ['frontend', 'client_side']; + +const treatmentResults: SplitIO.TreatmentsWithConfig = this.splitService.getTreatmentsWithConfigByFlagSets(flagSetsNames); + +// treatmentResults will have the following form: +// { +// FEATURE_FLAG_NAME_1: {treatment: 'on', +// config: "{ 'color' : 'red'}}", +// FEATURE_FLAG_NAME_2: {treatment: 'v2', +// config: "{ 'copy' : 'better copy'}}", +// } +``` + + + +### Shutdown + +Call the `splitService.destroy()` method before letting a process using the SDK exit, as this method gracefully shuts down the Split SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. + +```javascript title="TypeScript" +// You can just destroy and remove the variable reference and move on: +splitService.destroy(); +splitService = null; + +// destroy() returns a promise, so if you want to, for example, +// navigate to another page without losing impressions, you +// can do that once the promise resolves. +splitService.destroy().then(function() { + splitService = null; + + router.navigate(['/another']); +}); +``` + +After `destroy()` is called and finishes, any subsequent invocations to `getTreatment`/`getTreatments` or manager methods result in `control` or empty list, respectively. + +:::warning[Important!] +A call to the `destroy()` method also destroys the splitService object. When creating new client instance, first initialize the service again. +::: + +## Track + +Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your features on your users' actions and metrics. [Learn more about using track events](https://help.split.io/hc/en-us/articles/360020585772) in Split. + +In the examples below, you can see that the `.track()` method can take up to four arguments. The proper data type and syntax for each are: + +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: + * Contains 63 characters or fewer. + * Starts with a letter or number. + * Contains only letters, numbers, hyphen, underscore, or period. + * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` +* **VALUE:** (Optional) The value is used to create the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is **Integer** or **Float**. +* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. + +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the splitService was able to successfully queue the event to be sent back to Split's servers on the next event post. The service returns `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method has been provided. + +In the case that a bad input is provided, you can read more about our [SDK's expected behavior](https://help.split.io/hc/en-us/articles/360020585772-Track-events) + +```javascript title="TypeScript" +// The expected parameters are: +const queued: boolean = this.splitService.track('TRAFFIC_TYPE', 'EVENT_TYPE', eventValue, , { properties }); + +// Example with both a value and properties +const properties = {package : "premium", admin : true, discount : 50}; +const queued = this.splitService.track('user', 'page_load_time', 83.334, properties); + +// Example with only properties +const properties = {package : "premium", admin : true, discount : 50}; +const queued = this.splitService.track('user', 'page_load_time', null, properties); +``` + +## Configuration + +The SDK has a number of knobs for configuring performance. Each knob is tuned to a reasonable default. However, you can override the value while providing the config to the splitService.init method as shown in the Initialization section of this doc. To learn about the available configuration options, go to the [JavaScript SDK Configuration section.](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#configuration) + +## Manager + +To get a list of features available to the Split client, you can use the methods available on splitService as shown below: + +```javascript title="TypeScript" +import { SplitService } from '@splitsoftware/splitio-angular'; +this.splitService.init({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + storage: InLocalStorage({ + prefix: 'MY_PREFIX' + }) +}); +// Now use the service as usual +this.splitService.sdkReady$.subscribe(() => { + + // Get the array of feature flags data in SplitView format. + const views: SplitIO.SplitViews = + this.splitService.getSplits(); + + // Get the data of a specific feature flag in SplitView format. + const view: SplitIO.SplitView | null = + this.splitService.getSplit('billing_updates'); + + // Get the array of feature flag names. + const names: SplitIO.SplitNames = + this.splitService.getSplitNames(); + +}) +``` + +The `SplitView` object referenced above has the following structure: + +```typescript title="TypeScript" +type SplitView = { + name: string, + trafficType: string, + killed: boolean, + treatments: Array, + changeNumber: number, + configs: { + [treatmentName: string]: string + }, + sets: Array, + defaultTreatment: string +} +``` + +## Listener + +Split SDKs send impression data back to Split servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression listener*. For this purpose, the SDK's configurations have a parameter called `impressionListener` where an implementation of `ImpressionListener` could be added. This implementation **must** define the `logImpression` method and it receives data in the following schema. + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| impression | Object | Impression object that has the feature, key, treatment, label, etc. | +| attributes | Object | A map of attributes passed to `getTreatment`/`getTreatments` (if any). | +| sdkLanguageVersion | String| The version of the SDK. In this case the language is `angular` plus the version currently running. | + +:::info[Note] +There are two additional keys on this object, `ip` and `hostname`. They are not captured on the client side but kept for consistency. +::: + +## Implement custom impression listener + +The following is an example of how to implement a custom impression listener: + +```javascript title="TypeScript" +import { SplitService } from '@splitsoftware/splitio-angular'; + +class MyImprListener implements SplitIO.IImpressionListener { + logImpression(impressionData: SplitIO.ImpressionData) { + // do something with impressionData + } +} + +this.splitService.init({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + impressionListener: { + logImpression: new MyImprListener() + }) +}); +``` + +An impression listener is called asynchronously from the corresponding evaluation, but is almost immediate. + +Even though the SDK does not fail if there is an exception in the listener, do not block the call stack. + +## Logging + +To enable SDK logging in the browser, open your DevTools console and type the following: + +```javascript title="Enable logging from browser console" +// Acceptable values are 'DEBUG', 'INFO', 'WARN', 'ERROR' and 'NONE' +// Other acceptable values are 'on', 'enable' and 'enabled', which are equivalent to 'DEBUG' log level +localStorage.splitio_debug = 'on' +``` + +Reload the browser to start seeing the logs. + +You can also enable the logging via SDK settings and programmatically by calling the Logger API. + +```javascript title="Logger API (TypeScript)" +import { SplitFactory } from '@splitsoftware/splitio'; + +const sdk: SplitIO.ISDK = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + debug: true // Debug boolean option can be passed on settings. + // Acceptable values are: 'DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE'. + // It takes precedence over the localStorage flag. +}); +``` + +## Advanced use cases + +This section describes advanced use cases and features provided by the SDK. + +### Instantiate multiple SDK clients + +Split supports the ability to release based on multiple traffic types. For example, with traffic types, you can release to `users` in one feature flag and `accounts` in another. If you are unfamiliar with using multiple traffic types, refer to the [Traffic type guide](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) for more information. + +Each SDK client is tied to one specific customer ID at a time, so if you need to roll out features by different traffic types, instantiate multiple SDK clients, one for each traffic type. For example, you may want to roll out the feature `user-poll` by `users` and the feature `account-permissioning` by `accounts`. + +You can do this with the example below: + +```javascript title="TypeScript" +import { SplitService } from '@splitsoftware/splitio-angular'; + +this.splitService.init({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'CUSTOMER_ACCOUNT_ID' + // instantiate the sdk and service once and provide the ID for one of the + // traffic types that you plan to release to. It doesn't + // matter which you pick to start off with. + } +}); + +// to create another client for a User instead, just pass in a +// User ID to the splitService.initClient() method. This is only valid after +// at least one client has been initialized. +this.splitService.initClient('CUSTOMER_USER_ID'); + +// check treatment for user-poll and CUSTOMER_USER_ID +const user_poll_treatment: SplitIO.Treatment = + this.splitService.getTreatment('user-poll'); + +// check treatment for account-permissioning and CUSTOMER_ACCOUNT_ID +const account_permissioning_treatment: SplitIO.Treatment = + this.splitService.getTreatment('CUSTOMER_USER_ID','account-permissioning'); + +// track events for accounts +this.splitService.track('CUSTOMER_USER_ID','account', 'PAGELOAD', 7.86); + +// or track events for users +this.splitService.track('user', 'ACCOUNT_CREATED'); +``` + +In every getTreatment and track method, you can add an user id as first parameter to define which client to use. If a user id parameter is not present, splitService uses the one in SDK config. + +:::info[Number of SDK instances] +While the SDK does not put any limitations on the number of instances that can be created, we strongly recommend keeping the number of SDKs down to **one** or **two**. +::: + +### Subscribe to events + +You can subscribe to four different observables of the splitService. + +* `sdkReadyFromCache$`. This event fires once the SDK is ready to evaluate treatments using a version of your rollout plan cached in localStorage from a previous session (which might be stale). If there is data in localStorage, this event fires almost immediately, since access to localStorage is fast; otherwise, it doesn't fire. +* `sdkReady$`. This event fires once the SDK is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Split servers. +* `sdkReadyTimedOut$`. This event fires if there is no cached version of your rollout plan cached in localStorage, and the SDK could not download the data from Split servers within the time specified by the `readyTimeout` configuration parameter. This event does not indicate that the SDK initialization was interrupted. The SDK continues downloading the rollout plan and fires the `sdkReady$` event when finished. This delayed `sdkReady$` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. +* `sdkUpdate$`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in the Split user interface. + +The syntax to subscribe for each Observable is shown below: + +```javascript title="TypeScript" +function whenReady() { + const treatment: SplitIO.Treatment = this.splitService.getTreatment('FEATURE_FLAG_NAME'); + + if (treatment === 'on') { + // insert on code + } else if (treatment === 'off') { + // insert off code + } else { + // insert control code (usually the same as default treatment) + } +} + +// the service is ready for start making evaluations with your data +this.splitService.sdkReady$.subscribe(() => { + whenReady(); +} + +this.splitService.sdkReadyTimedOut$.subscribe(() => { + // this callback will be called after 1.5 seconds if and only if the client + // is not ready for that time. You can still call getTreatment() + // but it could return CONTROL. +} + +this.splitService.sdkUpdate$.subscribe(() => { + // fired each time the client state change. + // For example, when a feature flag or segment changes. + console.log('The SDK has been updated!'); +} + +// This event will fire only using the LocalStorage option and if there's Split data stored in the browser. +this.splitService.sdkReadyFromCache$.subscribe(() => { + // Fired after the SDK could confirm the presence of the Split data. + // This event fires really quickly, since there's no actual fetching of information. + // Keep in mind that data might be stale, this is NOT a replacement of sdkReady. +} +``` + +## Angular Guard + +These utilities provide an Angular Guard that allows you to avoid loading an angular component if the SDK is not ready. + +```javascript title="TypeScript" +import { SplitioGuard } from '@splitsoftware/splitio-angular'; + +const routes: Routes = [ + { + path: 'feature', + component: FeatureComponent, + // with this guard, featureComponent doesn't load until the SDK is ready + canActivate: [SplitioGuard] + } +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule] +}) +export class AppRoutingModule { } +``` + +## Example apps + +The following are example applications detailing how to configure and instantiate the Split Angular utilities on commonly used platforms. + +* [Angular](https://github.com/splitio/angular-sdk-examples) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/browser-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/browser-sdk.md new file mode 100644 index 00000000000..ecd21a6799e --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/browser-sdk.md @@ -0,0 +1,1357 @@ +--- +title: Browser SDK +sidebar_label: Browser SDK +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +This guide provides detailed information about our JavaScript Browser SDK, which is built on top of our JS SDK core modules but optimized for web browsers. + +The library exports a `slim` (default) and a `full` entry point in UMD, CommonJS, and ES module formats, which allows you to import and use the SDK. The `slim` entry point has less features by default, resulting in a smaller footprint. At the moment, the `full` entry point only differs from the `slim` entry point by including a [Fetch API polyfill](#language-support), but more features might be added in the future. + +Both entry points share the same pluggable API that you can use to include more functionality optionally and keep your bundle leaner. + +All of our SDKs are open source. Go to our [JavaScript Browser SDK GitHub repository](https://github.com/splitio/javascript-browser-client) to see the source code. + +:::info[Migrating from Browser SDK v0.x to Browser SDK v1.x] +Refer to this [migration guide](https://github.com/splitio/javascript-browser-client/blob/development/MIGRATION-GUIDE.md) for complete information on updating to v1.x. +::: + +:::info[Migrating JavaScript SDK to JavaScript Browser SDK] +Refer to the [**Browser SDK migration guide**](https://help.split.io/hc/en-us/articles/360059966112-Browser-SDK-Migration-Guide) if you are already using our [JavaScript SDK](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK) and want to migrate to JavaScript Browser SDK to take advantage of ES modules tree-shaking in your Web application project. +::: + +## Language support + +The JavaScript Browser SDK supports all major browsers. While the library was built to support ES5 syntax, it depends on native support for ES6 `Promise`, `Map` and `Set` objects, and therefore, you need to **polyfill** them if they are not available in your target browsers. + +You must polyfill Fetch Web API if it is not available in your target browsers and you are importing the SDK from the slim entry point. For more information, refer to the [Import the SDK into your project](#1-import-the-sdk-into-your-project) section of this guide. The `full` entry point doesn't require it. + +If you're looking for possible polyfill options, check [es6-promise](https://github.com/stefanpenner/es6-promise), [es6-map](https://github.com/medikoo/es6-map) and [es6-set](https://github.com/medikoo/es6-set) for Promise, Map and Set polyfills respectively. For Fetch Web API, we recommend the lightweight [unfetch](https://unpkg.com/unfetch@latest/polyfill/index.js) or [whatwg-fetch](https://cdn.jsdelivr.net/npm/whatwg-fetch@latest/dist/fetch.umd.min.js). + +## Initialization + +Set up Split in your code base with the following two steps: + +### 1. Import the SDK into your project + +You can import the SDK into your project using either of the two methods below, NPM or our bundled option which we host through our CDN. + +You can take advantage of both `slim` and `full` entry points when using NPM as you decide what to import. However, on the already bundled option, the code included is static, so we expose a different bundle for each entry point. The `slim` version includes all the key functionality of the SDK and the full one includes every available pluggable module, for example, the logger or the *InlocalStorage* module. + + + +```bash +npm install --save @splitsoftware/splitio-browserjs +``` + + +```html + + + + + + + +``` + + + +### 2. Instantiate the SDK and create a new Split client + + + +```javascript +// Instantiate the SDK. CDN exposes a splitio object globally with a reference to +// the SplitFactory (as well as any extra modules) + +var factory = splitio.SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + // key represents your internal user id, or the account id that + // the user belongs to. + // This could also be a cookie you generate for anonymous users + key: 'key' + } +}); + +// And get the client instance you'll use +var client = factory.client(); +``` + + +```javascript +var SplitFactory = require('@splitsoftware/splitio-browserjs').SplitFactory; + +// Or you can import the SplitFactory from the full entry point, +var SplitFactory = require('@splitsoftware/splitio-browserjs/full').SplitFactory; + +// Instantiate the SDK +var factory = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + // key represents your internal user id, or the account id that + // the user belongs to. + // This could also be a cookie you generate for anonymous users + key: 'key' + } +}); + +// And get the client instance you'll use +var client = factory.client(); +``` + + +```typescript +import { SplitFactory } from '@splitsoftware/splitio-browserjs'; + +// Or you can import the SplitFactory from the full entry point, +import { SplitFactory } from '@splitsoftware/splitio-browserjs/full'; + +// Instantiate the SDK +const factory: SplitIO.IBrowserSDK = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + // key represents your internal user id, or the account id that + // the user belongs to. + // This could also be a cookie you generate for anonymous users + key: 'key' + } +}); + +// And get the client instance you'll use +const client: SplitIO.IBrowserClient = factory.client(); +``` + + + +:::info[Notice for TypeScript] +With the SDK package on NPM, you get the SplitIO namespace, which contains useful types and interfaces for you to use. + +Feel free to dive into the declaration files if IntelliSense is not enough. +::: + +We recommend instantiating the Split factory once as a singleton and reusing it throughout your application. + +Configure the SDK with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split UI, on your Admin settings page, API keys section. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. + +## Using the SDK + +### Basic use + +When the SDK is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while its in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). + +To make sure the SDK is properly loaded before asking it for a treatment, block until the SDK is ready, as shown below. We set the client to listen for the `SDK_READY` event triggered by the SDK before asking for an evaluation. + +After the `SDK_READY` event fires, you can use the `getTreatment` method to return the proper treatment based on the `FEATURE_FLAG_NAME` and the `key` variables you passed when instantiating the SDK. + +Then use an if-else-if block as shown below and insert the code for the different treatments that you defined in the Split user interface. Remember the final else branch in your code to handle the client returning control. + + + +```javascript +client.on(client.Event.SDK_READY, function() { + var treatment = client.getTreatment('FEATURE_FLAG_NAME'); + + if (treatment == "on") { + // insert code here to show on treatment + } else if (treatment == "off") { + // insert code here to show off treatment + } else { + // insert your control treatment code here + } +}); +``` + + +```typescript +client.on(client.Event.SDK_READY, function() { + const treatment: SplitIO.Treatment = client.getTreatment('FEATURE_FLAG_NAME'); + + if (treatment == "on") { + // insert code here to show on treatment + } else if (treatment == "off") { + // insert code here to show off treatment + } else { + // insert your control treatment code here + } +}); +``` + + + +### Attribute syntax + +To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK's `getTreatment` method needs to be passed an attribute map at runtime. + +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in the Split user interface to decide whether to show the `on` or `off` treatment to this account. + +The `getTreatment` method has a number of variations that are described below. Each of these additionally has a variation that takes an attributes argument, which can defines attributes of the following types: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: + +* **Strings:** Use type String. +* **Numbers:** Use type Number. +* **Dates:** Use type Date and express the value in `milliseconds since epoch`.
**Note:** Milliseconds since epoch is expressed in UTC. If your date or date-time combination is in a different timezone, first convert it to UTC, then transform it to milliseconds since epoch. +* **Booleans:** Use type Boolean. +* **Sets:** Use type Array. + + + +```javascript +var attributes = { + // date attributes are handled as `millis since epoch` + registered_date: new Date('YYYY-MM-DDTHH:mm:ss.sssZ').getTime(), + // this string will be compared against a list called `plan_type` + plan_type: 'growth', + // this number will be compared agains a const value called `deal_size` + deal_size: 10000, + // this boolean will be compared against a const value called `paying_customer` + paying_customer: true, + // this array will be compared against a set called `permissions` + permissions: ["read", "write"] +}; + +var treatment = client.getTreatment('FEATURE_FLAG_NAME', attributes); + +if (treatment === 'on') { + // insert on code here +} else if (treatment === 'off') { + // insert off code here +} else { + // insert control code here +} +``` + + +```typescript +const attributes: SplitIO.Attributes = { + // date attributes are handled as `millis since epoch` + registered_date: new Date('YYYY-MM-DDTHH:mm:ss.sssZ').getTime(), + // this string will be compared against a list called `plan_type` + plan_type: 'growth', + // this number will be compared agains a const value called `deal_size` + deal_size: 10000, + // this array will be compared against a set called `permissions` + permissions: [‘read’, ‘write’] +}; + +const treatment: SplitIO.Treatment = client.getTreatment('FEATURE_FLAG_NAME', attributes); + +if (treatment === 'on') { + // insert on code here +} else if (treatment === 'off') { + // insert off code here +} else { + // insert control code here +} +``` + + + +You can pass your attributes in exactly this way to the `client.getTreatments` method. + +### Binding attributes to the client + +Attributes can optionally be bound to the client at any time during the SDK lifecycle. These attributes are stored in memory and used in every evaluation to avoid the need to keep the attribute set accessible through the whole app. When an evaluation is called, the attributes provided (if any) at evaluation time are combined with the ones that are already loaded into the SDK memory, with the ones provided at function execution time taking precedence. This enables those attributes to be overridden or hidden for specific evaluations. + +An attribute is considered valid if it follows one of the types listed below: +- String +- Number +- Boolean +- Array + +The SDK validates these before storing them and if there are invalid or missing values, possibly indicating an issue, the methods return the boolean `false` and do not update any value. + +To use these methods, refer to the example below: + +```javascript title="JavaScript" +var attributes = { + registered_date: new Date('YYYY-MM-DDTHH:mm:ss.sssZ').getTime(), + plan_type: 'growth', + deal_size: 10000, + paying_customer: true, + permissions: ["read", "write"] +}; + +// set attributes returns true unless there is an issue storing it +var result = client.setAttributes(attributes); + +// set one attribute and returns true unless there is an issue storing it +var result = client.setAttribute('paying_customer', false); + +// Get an attribute +var plan_type = client.getAttribute('plan_type'); + +// Get all attributes +var stored_attributes = client.getAttributes(); + +// Remove an attribute +var result = client.removeAttribute('permissions'); + +// Remove all attributes +var result = client.clearAttributes(); +``` + +### Multiple evaluations at once + +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` from the Split client to do this. +* `getTreatments`: Pass a list of the feature flag names you want treatments for. +* `getTreatmentsByFlagSet`: Evaluate all flags that are part of the provided set name and are cached on the SDK instance. +* `getTreatmentsByFlagSets`: Evaluate all flags that are part of the provided set names and are cached on the SDK instance. + + + +```javascript +// Getting treatments by feature flag names +var flagNames = ['FEATURE_FLAG_NAME_1', 'FEATURE_FLAG_NAME_2']; +var treatments = client.getTreatments(flagNames); + +// Getting treatments by set +treatments = client.getTreatmentsByFlagSet('frontend'); + +// Getting treatments for the union of multiple sets +var flagSets = ['frontend', 'client_side']; +treatments = client.getTreatmentsByFlagSets(flagSets); + +// treatments have the following form: +// { +// FEATURE_FLAG_NAME_1: 'on', +// FEATURE_FLAG_NAME_2: 'visa' +// } +``` + + +```typescript +// Getting treatments by feature flag names +const flagNames = ['FEATURE_FLAG_NAME_1', 'FEATURE_FLAG_NAME_2']; +let treatments: SplitIO.Treatments = client.getTreatments(flagNames); + +// Getting treatments by set +treatments = client.getTreatmentsByFlagSet('frontend'); + +// Getting treatments for the union of multiple sets +const flagSets = ['frontend', 'client_side']; +treatments = client.getTreatmentsByFlagSets(flagSets); + +// treatments have the following form: +// { +// FEATURE_FLAG_NAME_1: 'on', +// FEATURE_FLAG_NAME_2: 'visa' +// } +``` + + + +### Get treatments with configurations + +To [leverage dynamic configurations with your treatments](https://help.split.io/hc/en-us/articles/360026943552), you should use the `getTreatmentWithConfig` method. + +This method will return an object with the structure below: + +```typescript title="TypeScript" +type TreatmentResult = { + treatment: string, + config: string | null +}; +``` + +As you can see from the object structure, the config is a stringified version of the configuration JSON defined in the Split user interface. If there is no configuration defined for a treatment, the SDK returns `null` for the config parameter. This method takes the exact same set of arguments as the standard `getTreatment` method. See below for examples on proper usage: + + + +```javascript +var treatmentResult = client.getTreatmentWithConfig('FEATURE_FLAG_NAME', attributes); +var configs = JSON.parse(treatmentResult.config); +var treatment = treatmentResult.treatment; + +if (treatment === 'on') { + // insert on code here and use configs here as necessary +} else if (treatment === 'off') { + // insert off code here and use configs here as necessary +} else { + // insert control code here +} +``` + + +```typescript +const treatmentResult: SplitIO.TreatmentWithConfig = client.getTreatmentWithConfig('FEATURE_FLAG_NAME', attributes); +const configs = JSON.parse(treatmentResult.config); +const treatment = treatmentResult.treatment; + +if (treatment === 'on') { + // insert on code here and use configs here as necessary +} else if (treatment === 'off') { + // insert off code here and use configs here as necessary +} else { + // insert control code here +} +``` + + + +If you need to get multiple evaluations at once, you can also use the `getTreatmentsWithConfig` methods. These methods take the exact same arguments as the [getTreatments](#multiple-evaluations-at-once) methods but return a mapping of feature flag names to TreatmentResults instead of strings. Example usage below: + + + +```javascript +// Getting treatments by feature flag names +var flagNames = ['FEATURE_FLAG_NAME_1', 'FEATURE_FLAG_NAME_2']; +var treatmentResults = client.getTreatmentsWithConfig(flagNames); + +// Getting treatments by set +treatmentResults = client.getTreatmentsWithConfigByFlagSet('frontend'); + +// Getting treatments for the union of multiple sets +var flagSets = ['frontend', 'client_side']; +treatmentResults = client.getTreatmentsWithConfigByFlagSets(flagSets); + +// treatmentResults will have the following form: +// { +// FEATURE_FLAG_NAME_1: {treatment: 'on', +// config: "{ 'color' : 'red'}}", +// FEATURE_FLAG_NAME_2: {treatment: 'v2', +// config: "{ 'copy' : 'better copy'}}", +// } +``` + + +```typescript +// Getting treatments by feature flag names +const flagNames = ['FEATURE_FLAG_NAME_1', 'FEATURE_FLAG_NAME_2']; +let treatmentResults: SplitIO.TreatmentsWithConfig = client.getTreatmentsWithConfig(flagNames); + +// Getting treatments by set +treatmentResults = client.getTreatmentsWithConfigByFlagSet('frontend'); + +// Getting treatments for the union of multiple sets +const flagSets = ['frontend', 'client_side']; +treatmentResults = client.getTreatmentsWithConfigByFlagSets(flagSets); + +// treatmentResults will have the following form: +// { +// FEATURE_FLAG_NAME_1: {treatment: 'on', +// config: "{ 'color' : 'red'}}", +// FEATURE_FLAG_NAME_2: {treatment: 'v2', +// config: "{ 'copy' : 'better copy'}}", +// } +``` + + + +### Shutdown + +Call the `client.destroy()` method before letting a process using the SDK exit, as this method gracefully shuts down the Split SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. + + + +```javascript +// You can just destroy and remove the variable reference and move on: +user_client.destroy(); +user_client = null; + +// destroy() returns a promise, so if you want to, for example, +// navigate to another page without losing impressions, you +// can do that once the promise resolves. +user_client.destroy().then(function() { + user_client = null; + + document.location.replace('another_page'); +}); +``` + + + +After `destroy()` is called and finishes, any subsequent invocations to `getTreatment`/`getTreatments` or manager methods result in `control` or empty list, respectively. + +:::warning[Important!] +A call to the `destroy()` method also destroys the factory object. When creating new client instance, first create a new factory instance. +::: + +## Track + +Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users’ actions and metrics. [Learn more about using track events](https://help.split.io/hc/en-us/articles/360020585772) in Split. + +In the examples below, you can see that the `.track()` method can take up to four arguments. The proper data type and syntax for each are: + +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: + * Contains 63 characters or fewer. + * Starts with a letter or number. + * Contains only letters, numbers, hyphen, underscore, or period. + * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` +* **VALUE:** (Optional) The value to be used in creating the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is **Integer** or **Float**. +* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. + +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event to be sent back to Split's servers on the next event post. The SDK returns `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method has been provided. + +In the case that a bad input has been provided, you can read more about our [SDK's expected behavior](https://help.split.io/hc/en-us/articles/360020585772-Track-events) + + + +```javascript +// The expected parameters are: +var queued = client.track('TRAFFIC_TYPE', 'EVENT_TYPE', eventValue, { properties }); + +// Example with both a value and properties +var properties = {package : "premium", admin : true, discount : 50}; +var queued = client.track('user', 'page_load_time', 83.334, properties); + +// Example with only properties +var properties = {package : "premium", admin : true, discount : 50}; +var queued = client.track('user', 'page_load_time', null, properties); +``` + + +```typescript +// The expected parameters are: +const queued: boolean = client.track('TRAFFIC_TYPE', 'EVENT_TYPE', eventValue, , { properties }); + +// Example with both a value and properties +const properties = {package : "premium", admin : true, discount : 50}; +const queued = client.track('user', 'page_load_time', 83.334, properties); + +// Example with only properties +const properties = {package : "premium", admin : true, discount : 50}; +const queued = client.track('user', 'page_load_time', null, properties); +``` + + + +## Configuration + +The SDK has a number of knobs for configuring performance. Each knob is tuned to a reasonable default. However, you can override the value while instantiating the SDK. The parameters available for configuration are shown below. + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| core.labelsEnabled | Enable impression labels from being sent to Split cloud. Labels may contain sensitive information. | true | +| startup.readyTimeout | Maximum amount of time in seconds to wait before firing the `SDK_READY_TIMED_OUT` event | 10 | +| startup.requestTimeoutBeforeReady | The SDK has two main endpoints it uses /splitChanges and /mySegments that it hits to get ready. This config sets how long (in seconds) the SDK will wait for each request it makes as part of getting ready. | 5 | +| startup.retriesOnFailureBeforeReady | How many retries on /splitChanges and /mySegments we will do while getting the SDK ready | 1 | +| startup.eventsFirstPushWindow | Use to set a specific timer (expressed in seconds) for the first push of events, starting on SDK initialization. | 10 | +| scheduler.featuresRefreshRate | The SDK polls Split servers for changes to feature rollout plans. This parameter controls this polling period in seconds. | 60 | +| scheduler.segmentsRefreshRate | The SDK polls Split servers for changes to segment definitions. This parameter controls this polling period in seconds. | 60 | +| scheduler.impressionsRefreshRate | The SDK sends information on who got what treatment at what time back to Split servers to power analytics. This parameter controls how often this data is sent to Split servers. The parameter should be in seconds. | 300 | +| scheduler.impressionsQueueSize | The max amount of impressions we queue. If the queue is full, the SDK flushes the impressions and resets the timer. | 30000 | +| scheduler.eventsPushRate | The SDK sends tracked events to Split servers. This setting controls that flushing rate in seconds. | 60 | +| scheduler.eventsQueueSize | The max amount of events we queue. If the queue is full, the SDK flushes the events and resets the timer. | 500 | +| scheduler.telemetryRefreshRate | The SDK caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers (in seconds). | 3600 seconds (1 hour) | +| sync.splitFilters | Filter specific feature flags to be synced and evaluated by the SDK. This is formed by a type string property and a list of string values for the given criteria. Using the types 'bySet' (recommended, flag sets are available in all tiers) or 'byName', pass an array of strings defining the query. If empty or unset, all feature flags are downloaded by the SDK. | [] | +| sync.impressionsMode | This configuration defines how impressions (decisioning events) are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Split; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, ALL impressions are queued and sent to Split; this is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. | `OPTIMIZED` | +| sync.enabled | Controls the SDK continuous synchronization flags. When `true`, a running SDK processes rollout plan updates performed in the Split user interface (default). When `false`, it fetches all data upon init, which ensures a consistent experience during a user session and optimizes resources when these updates are not consumed by the app. | true | +| sync.requestOptions.getHeaderOverrides | A callback function that can be used to override the Authentication header or append new headers to the SDK's HTTP(S) requests. | undefined | +| storage | Pluggable storage instance to be used by the SDK as a complement to in memory storage. Only supported option today is `InLocalStorage`. Read more [here](#configuring-localstorage-cache-for-the-sdk). | In memory storage | +| debug | Either a boolean flag, string log level or logger instance for activating SDK logs. See [logging](#logging) for details. | false | +| streamingEnabled | Boolean flag to enable the streaming service as default synchronization mechanism. In the event of an issue with streaming, the SDK will fallback to the polling mechanism. If false, the SDK will poll for changes as usual without attempting to use streaming. | true | +| userConsent | User consent status used to control the tracking of events and impressions. Possible values are `GRANTED`, `DECLINED`, and `UNKNOWN`. See [User consent](#user-consent) for details. | `GRANTED` | + +To set each of the parameters defined above, use the following syntax: + + + +```javascript +var sdk = SplitFactory({ + startup: { + readyTimeout: 10, // 10 sec + eventsFirstPushWindow: 10 // 10 sec + }, + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'YOUR_KEY' + }, + scheduler: { + featuresRefreshRate: 5, // 5 sec + segmentsRefreshRate: 60, // 60 sec + impressionsRefreshRate: 300, // 300 sec + impressionsQueueSize: 30000, // 30000 items + eventsPushRate: 60, // 60 sec + eventsQueueSize: 500, // 500 items + telemetryRefreshRate: 3600 // 1 hour + }, + sync: { + splitFilters: [{ + type: 'bySet', + values: ['frontend'] + }], + impressionsMode: 'OPTIMIZED' + }, + debug: false +}); +``` + + +```typescript +const sdk: SplitIO.IBrowserSDK = SplitFactory({ + startup: { + readyTimeout: 10, // 10 sec + eventsFirstPushWindow: 10 // 10 sec + }, + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'YOUR_KEY' + }, + scheduler: { + featuresRefreshRate: 5, // 5 sec + segmentsRefreshRate: 60, // 60 sec + impressionsRefreshRate: 300, // 300 sec + impressionsQueueSize: 30000, // 30000 items + eventsPushRate: 60, // 60 sec + eventsQueueSize: 500, // 500 items + telemetryRefreshRate: 3600 // 1 hour + }, + sync: { + splitFilters: [{ + type: 'bySet', + values: ['frontend'] + }], + impressionsMode: 'OPTIMIZED' + }, + debug: false +}); +``` + + + +### Configuring LocalStorage cache for the SDK + +To use the pluggable `InLocalStorage` option of the SDK and be able to cache flags for subsequent loads in the same browser, you need to pass it to the SDK config on its `storage` option. + +This `InLocalStorage` function accepts an optional object with options described below: + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| prefix | An optional prefix for your data, to avoid collisions. | `SPLITIO` | + +These pluggable caches are always available on NPM, but if using the CDN you need the full bundle. Refer to the [Import the SDK into your project](#1-import-the-sdk-into-your-project) section for more information. + + + +```javascript +var factory = window.splitio.SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + // Same as SplitFactory, InLocalStorage is exposed on the global splitio object + storage: window.splitio.InLocalStorage({ + prefix: 'MY_PREFIX' + }) +}); + +// Now use the SDK as usual +var client = factory.client(); +``` + + +```javascript +import { SplitFactory, InLocalStorage } from '@splitsoftware/splitio-browserjs'; + +const factory: SplitIO.IBrowserSDK = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + storage: InLocalStorage({ + prefix: 'MY_PREFIX' + }) +}); + +// Now use the SDK as usual +const client = factory.client(); +``` + + + +## Localhost mode + +For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the Split SDK can be started in **localhost** mode (aka off-the-grid or offline mode). In this mode, the SDK neither polls nor updates Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the features. + +Define the feature flags you want to use in the `features` object map. All `getTreatment` calls for a feature flag now only return the one treatment (and config, if defined) that you have defined in the map. +Any feature that is not provided in the `features` map returns the [control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment) if the SDK was asked to evaluate them. + +You can use the additional configuration parameters below when instantiating the SDK in `localhost` mode. + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| scheduler.offlineRefreshRate | The refresh interval for the mocked features treatments. | 15 | +| features | A fixed mapping of which treatment to show for our mocked features. | {}
By default we have no mocked features. | + +To use the SDK in localhost mode, replace the SDK key on `authorizationKey` property with `'localhost'`, as shown in the example below. Note that you can define in the `features` object a feature flag name and its treatment directly or use a map to define both a treatment and a dynamic configuration. + +If you define just a string as the value for a feature flag name, any config returned by our SDKs are always null. If you use a map, we return the specified treatment and the specified config (which can also be null). + + + +```javascript + + +var sdk = splitio.SplitFactory({ + core: { + authorizationKey: 'localhost' + }, + features: { + 'reporting_v2': 'on', // example with just a string value for the treatment + 'billing_updates': { treatment: 'visa', config: '{ "color": "blue" }' }, // example of a defined config + 'show_status_bar': { treatment: 'off', config: null } // example of a null config + }, + scheduler: { + offlineRefreshRate: 15 // 15 sec + } +}); + +var client = sdk.client(); + +// The following code will be evaluated once the engine finishes the initialization +client.on(client.Event.SDK_READY, function() { + // The sentence below will return 'on' + var t1 = client.getTreatment('reporting_v2') + // The sentence below will return an object with the structure of: {treatment:'visa',config:'{ "color":"blue" }'} + var t2 = client.getTreatmentWithConfig('billing_updates') + // The sentence below will return 'control' because that feature does not exist + var t3 = client.getTreatmentWithConfig('navigation_bar_changes') +}); +``` + + +```typescript +import { SplitFactory } from '@splitsoftware/splitio-browserjs'; + +const sdk: SplitIO.IBrowserSDK = SplitFactory({ + core: { + authorizationKey: 'localhost' + }, + features: { + 'reporting_v2': 'on', // example with just a string value for the treatment + 'billing_updates': { treatment: 'visa', config: '{ "color": "blue"}' } // example of a defined config + 'show_status_bar': { treatment: 'off', config: null } // example of a null config + }, + scheduler: { + offlineRefreshRate: 15 // 15 sec + } +}); + +const client: SplitIO.IBrowserClient = sdk.client(); + +// The following code will be evaluated once the engine finishes the initialization +client.on(client.Event.SDK_READY, () => { + // The sentence below will return 'on' + const t1: SplitIO.Treatment = client.getTreatment('reporting_v2'); + // The sentence below will return an object with the structure of: {treatment:'visa',config:'{ "color":"blue" }' + const t2: SplitIO.Treatment = client.getTreatmentWithConfig('billing_updates'); + // The sentence below will return 'control' because that feature does not exist + const t3: SplitIO.Treatment = client.getTreatmentWithConfig('navigation_bar_changes'); +}); +``` + + + +You can then change the feature flags as necessary for your testing, by mutating the properties of the `features` object you've provided. The SDK simulates polling for changes every `offlineRefreshRate` seconds, and will emit an `SDK_UPDATE` event if the mocked features have changed. + +```javascript title="JavaScript" +// The SDK keeps a reference to the `features` object map, so you can mutate the object as follows to emit SDK_UPDATE events: +config.features['reporting_v2'] = 'off'; // update reporting_v2 +config.features['reporting_v3'] = 'off'; // add reporting_v3 +delete config.features['reporting_v2']; // delete reporting_v2 + +// In case you need to update the whole mock object, you can replace the internal reference from the factory: +factory.settings.features = { 'reporting_v3': 'off' }; + +// But don't do it on the passed configuration, as the SDK will not reference the new object: +config.features = { 'reporting_v3': 'off' }; // Will not emit SDK_UPDATE +``` + +## Manager + +Use the Split Manager to get a list of features available to the Split client. To instantiate a Manager in your code base, use the same factory that you used for your client: + + + +```javascript +var factory = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + // the key can be the logged in + // user id, or the account id that + // the logged in user belongs to. + // The type of customer (user, account, custom) + // is chosen during Split's sign-up process. + key: 'key' + } +}); + +var manager = factory.manager(); + +manager.once(manager.Event.SDK_READY, function() { + // Once it's ready, use the manager +}); +``` + + +```typescript +const factory: SplitIO.IBrowserSDK = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + // the key can be the logged in + // user id, or the account id that + // the logged in user belongs to. + // The type of customer (user, account, custom) + // is chosen during Split's sign-up process. + key: 'key' + } +}); + +const manager: SplitIO.IManager = factory.manager(); + +manager.once(manager.Event.SDK_READY, function() { + // Once it's ready, use the manager +}); +``` + + + +The Manager has the following methods available: + + + +```javascript +/** + * Returns the feature flag registered within the SDK that matches this name. + * + * @return SplitView or null. + */ +var splitView = manager.split('name-of-feature-flag'); + +/** + * Retrieves all the feature flags that are currently registered within the SDK. + * + * returns a List of SplitViews. + */ +var splitViewsList = manager.splits(); + +/** + * Returns the names of all feature flags registered within the SDK. + * + * @return a List of Strings of the features' names. + */ +var splitNamesList = manager.names(); +``` + + +```typescript +/** + * Returns the feature flag registered within the SDK that matches this name. + * + * @return SplitView or null. + */ +const splitView: SplitIO.SplitView = manager.split('name-of-feature-flag'); + +/** + * Retrieves all the feature flags that are currently registered within the SDK. + * + * returns a List of SplitViews. + */ +const splitViewsList: SplitIO.SplitViews = manager.splits(); + +/** + * Returns the names of all feature flags registered within the SDK. + * + * @return a List of Strings of the features' names. + */ +const splitNamesList: SplitIO.SplitNames = manager.names(); +``` + + + +The `SplitView` object referenced above has the following structure: + +```typescript title="TypeScript" +type SplitView = { + name: string, + trafficType: string, + killed: boolean, + treatments: Array, + changeNumber: number, + configs: { + [treatmentName: string]: string + }, + defaultTreatment: string, + sets: Array, + impressionsDisabled: boolean +} +``` + +## Listener + +Split SDKs send impression data back to Split servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression listener*. For that purpose, the SDK's configurations have a parameter called `impressionListener` where an implementation of `ImpressionListener` could be added. This implementation **must** define the `logImpression` method and it receives data in the following schema. + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| impression | Object | Impression object that has the feature, key, treatment, label, etc. | +| attributes | Object | A map of attributes passed to `getTreatment`/`getTreatments` (if any). | +| sdkLanguageVersion | String| The version of the SDK. In this case the language is `browserjs` plus the version currently running. | + +:::info[Note] +There are two additional keys on this object, `ip` and `hostname`. They are not captured on the client side but kept for consistency. +::: + +## Implement custom impression listener + +The following is an example of how to implement a custom impression listener: + + + +```javascript +function logImpression(impressionData) { + // do something with the impression data. +} + +var factory = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + impressionListener: { + logImpression: logImpression + } +}); +``` + + +```typescript +class MyImprListener implements SplitIO.IImpressionListener { + logImpression(impressionData: SplitIO.ImpressionData) { + // do something with impressionData + } +} + +const factory: SplitIO.IBrowserSDK = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + impressionListener: { + logImpression: new MyImprListener() + } +}); +``` + + + +An impression listener is called asynchronously from the corresponding evaluation, but is almost immediate. + +Even though the SDK does not fail if there is an exception in the listener, do not block the call stack. + +## Logging + +To trim as many bits as possible from the user application builds, we divided the logger in implementations that contain the log messages for each log level: `ErrorLogger`, `WarnLogger`, `InfoLogger`, and `DebugLogger`. Higher log level options contain the messages for the lower ones, with DebugLogger containing them all. To enable descriptive SDK logging, you need to plug in a logger instance as shown below: + + + +```javascript +import { SplitFactory, DebugLogger } from '@splitsoftware/splitio-browserjs'; + +const sdk: SplitIO.IBrowserSDK = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + debug: DebugLogger() // other options are `InfoLogger`, `WarnLogger` and `ErrorLogger` +}); +``` + + +```javascript +var splitio = require('@splitsoftware/splitio-browserjs'); + +var sdk = splitio.SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + debug: splitio.DebugLogger() // other options are `InfoLogger`, `WarnLogger` and `ErrorLogger` +}); +``` + + +```javascript +var sdk = splitio.SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + debug: splitio.DebugLogger() // Only the full UMD bundle includes `DebugLogger`, `InfoLogger`, `WarnLogger` and `ErrorLogger` modules in the `splitio` namespace. +}); +``` + + + +You can also enable the SDK logging via a boolean or log level value as `debug` settings, and change it dynamically by calling the SDK Logger API. However, in any case where the proper logger instance is not plugged in, instead of a human readable message, you'll get a code and optionally some params for the log itself. While these logs would be enough for the Split support team, if you find yourself in a scenario where you need to parse this information, you can check the constant files in our javascript-commons repository (where you have tags per version if needed) under the [logger folder](https://github.com/splitio/javascript-commons/blob/master/src/logger/). + + + +```javascript +import { SplitFactory } from '@splitsoftware/splitio-browserjs'; + +const sdk: SplitIO.IBrowserSDK = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + debug: true // other options are 'ERROR', 'WARN', 'INFO' and 'DEBUG +}); + +// Or you can use the Logger API methods which have an immediate effect. +sdk.Logger.setLogLevel('WARN'); // Acceptable values are: 'DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE' +sdk.Logger.enable(); // equivalent to `setLogLevel('DEBUG')` +sdk.Logger.disable(); // equivalent to `setLogLevel('NONE')` +``` + + +```javascript +var splitio = require('@splitsoftware/splitio-browserjs'); + +var sdk = splitio.SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + debug: true // other options are 'ERROR', 'WARN', 'INFO' and 'DEBUG +}); + +// Or you can use the Logger API methods which have an immediate effect. +sdk.Logger.setLogLevel('WARN'); // Acceptable values are: 'DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE' +sdk.Logger.enable(); // equivalent to `setLogLevel('DEBUG')` +sdk.Logger.disable(); // equivalent to `setLogLevel('NONE')` +``` + + + +SDK logging can also be globally enabled via a localStorage value by opening your DevTools console and typing the following: + +```javascript title="Enable logging from browser console" +// Acceptable values are 'DEBUG', 'INFO', 'WARN', 'ERROR' and 'NONE' +// Other acceptable values are 'on', 'enable' and 'enabled', which are equivalent to 'DEBUG' log level +localStorage.splitio_debug = 'on' +``` + +## Advanced use cases + +This section describes advanced use cases and features provided by the SDK. + +### Instantiate multiple SDK clients + +Split supports the ability to release based on multiple traffic types. For example, with traffic types, you can release to `users` in one feature flag and `accounts` in another. If you are unfamiliar with using multiple traffic types, refer to the [Traffic type guide](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) for more information. + +Each SDK client is tied to one specific customer ID at a time, so if you need to roll out feature flags by different traffic types, instantiate multiple SDK clients, one for each traffic type. For example, you may want to roll out the feature `user-poll` by `users` and the feature `account-permissioning` by `accounts`. + +You can do this with the example below: + + + +```javascript +var factory = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'CUSTOMER_ACCOUNT_ID', + // Instantiate the sdk once and provide the ID for one of the + // traffic types that you plan to release to. It doesn't + // matter which you pick to start off with. + }, +}); + +// now when you call factory.client(), the sdk creates a client +// using the Account ID and traffic type name (if any) +// you passed in during the factory creation. +var account_client = factory.client(); + +// to create another client for a User instead, just pass in a User ID + +// This is only valid after at least one client has been initialized. +var user_client = factory.client('CUSTOMER_USER_ID'); + +// check treatment for user-poll and CUSTOMER_USER_ID +var user_poll_treatment = user_client.getTreatment('user-poll'); + +// check treatment for account-permissioning and CUSTOMER_ACCOUNT_ID +var account_permissioning_treatment = account_client.getTreatment('account-permissioning'); + +// track events for accounts +user_client.track('account', 'PAGELOAD', 7.86); + +// or track events for users +account_client.track('user', 'ACCOUNT_CREATED'); +``` + + +```typescript +const sdk: SplitIO.IBrowserSDK = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'CUSTOMER_ACCOUNT_ID' + // instantiate the sdk once and provide the ID for one of the + // traffic types that you plan to release to. It doesn't + // matter which you pick to start off with. + }, +}); + +// now when you call sdk.client(), the sdk will create a client +// using the Account ID you passed in during the factory creation. +const account_client: SplitIO.IBrowserClient = factory.client(); + +// to create another client for a User instead, just pass in a +// User ID to the sdk.client() method. This is only valid after +// at least one client has been initialized. +const user_client: SplitIO.IBrowserClient = + factory.client('CUSTOMER_USER_ID'); + +// check treatment for user-poll and CUSTOMER_USER_ID +const user_poll_treatment: SplitIO.Treatment = + user_client.getTreatment('user-poll'); + +// check treatment for account-permissioning and CUSTOMER_ACCOUNT_ID +const account_permissioning_treatment: SplitIO.Treatment = + account_client.getTreatment('account-permissioning'); + +// track events for accounts +user_client.track('account', 'PAGELOAD', 7.86); + +// or track events for users +account_client.track('user', 'ACCOUNT_CREATED'); +``` + + + +:::info[Number of SDK instances] +While the SDK does not put any limitations on the number of instances that can be created, we strongly recommend keeping the number of SDKs down to **one** or **two**. +::: + +### Subscribe to events + +You can listen for four different events from the SDK. + +* `SDK_READY_FROM_CACHE`. This event fires once the SDK is ready to evaluate treatments using a version of your rollout plan cached in localStorage from a previous session (which might be stale). If there is data in localStorage, this event fires almost immediately, since access to localStorage is fast; otherwise, it doesn't fire. +* `SDK_READY`. This event fires once the SDK is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Split servers. +* `SDK_READY_TIMED_OUT`. This event fires if there is no cached version of your rollout plan cached in localStorage, and the SDK could not download the data from Split servers within the time specified by the `readyTimeout` configuration parameter. This event does not indicate that the SDK initialization was interrupted. The SDK continues downloading the rollout plan and fires the `SDK_READY` event when finished. This delayed `SDK_READY` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. +* `SDK_UPDATE`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in the Split user interface. + +The syntax to listen for each event is shown below: + + + +```javascript +function whenReady() { + var treatment = client.getTreatment('FEATURE_FLAG_NAME'); + if (treatment === 'on') { + // insert on code + } else if (treatment === 'off') { + // insert off code + } else { + // insert control code (usually the same as default treatment) + } +} +client.once(client.Event.SDK_READY, function () { + // the client is ready to evaluate treatments according to the latest feature flag definitions +}); + +client.once(client.Event.SDK_READY_TIMED_OUT, function () { + // this callback will be called after the set timeout period has elapsed if and only if the client + // is not ready for that time. You can still call getTreatment() + // but it could return CONTROL. +}); + +client.on(client.Event.SDK_UPDATE, function () { + // fired each time the client state changes. + // For example, when a feature flag or a segment changes. + console.log('The SDK has been updated!'); +}); + +// This event fires only using the LocalStorage option and if there's Split data stored in the browser. +client.once(client.Event.SDK_READY_FROM_CACHE, function () { + // Fired after the SDK could confirm the presence of the Split data. + // This event fires really quickly, since there's no actual fetching of information. + // Keep in mind that data might be stale, this is NOT a replacement of SDK_READY. +}); +``` + + +```typescript +function whenReady() { + const treatment: SplitIO.Treatment = client.getTreatment('FEATURE_FLAG_NAME'); + + if (treatment === 'on') { + // insert on code + } else if (treatment === 'off') { + // insert off code + } else { + // insert control code (usually the same as default treatment) + } +} + +// the client is ready for start making evaluations with your data +client.once(client.Event.SDK_READY, whenReady); + +client.once(client.Event.SDK_READY_TIMED_OUT, () => { + // this callback will be called after 1.5 seconds if and only if the client + // is not ready for that time. You can still call getTreatment() + // but it could return CONTROL. +}); + +client.on(client.Event.SDK_UPDATE, () => { + // fired each time the client state change. + // For example, when a feature flag or a segment changes. + console.log('The SDK has been updated!'); +}); + +// This event fires only using the LocalStorage option and if there's Split data stored in the browser. +client.once(client.Event.SDK_READY_FROM_CACHE, function () { + // Fired after the SDK could confirm the presence of the Split data. + // This event fires really quickly, since there's no actual fetching of information. + // Keep in mind that data might be stale, this is NOT a replacement of SDK_READY. +}); +``` + + + +### Sharing state with a pluggable storage + +By default, the SDK fetches the feature flags and segments it needs to compute treatments from Split cloud, and stores it in its cache. As a result, this makes it easy to get set up with Split by instantiating the SDK, waiting for the `SDK_READY` event, and starting to use it. This default execution mode, called standalone mode, is appropriate for mobile and Web apps running on the client-side. However, in a stateless environment, like many serverless or edge computing solutions, this model could lead to some performance implications. + +Unlike a Web app or a traditional server process, code running in a stateless environment generally has a lifecycle that is much shorter because it is associated to the lifetime of a single HTTP request/response cycle. Therefore, for each incoming HTTP request, the code must instantiate the SDK and wait until it fetches its state before evaluating, which impacts the response latency. Also, the pricing model of serverless providers usually depends on the average duration of your code and the outgoing HTTP requests. + +To optimize latency, externalize the state of the SDK in a data storage available on the same infrastructure where SDKs are instantiated and instruct the SDKs to "consume" data from that storage instead of fetching it from Split cloud. This is known as consumer mode, which has two variants, *consumer* and *partial consumer*, as illustrated in the following diagram. + +

+ sdk_modes.png +

+ +#### Synchronizing Split data on your storage + +As illustrated in the previous diagram, running the SDK in consumer mode requires an additional component for synchronizing the Split data in your storage, which is known as [Synchronizer](https://help.split.io/hc/en-us/articles/4421513571469). + +#### Consumer modes + +In *consumer* and *partial consumer* modes, the SDK evaluates treatments by retrieving rollout plans from a shared data storage. + +The difference between each mode is in how generated impressions and events are handled: +* In consumer mode, the SDK uses the shared storage to store impressions and events, instead of submitting them directly to Split cloud. The synchronizer is in charge of submitting this data to Split. +* In partial consumer mode, the SDK behaves as in standalone mode, submitting events and impressions to Split cloud. In this mode, [configuration parameters](#configuration) that affects how events and impressions are submitted, such as changing a push rate or the events queue size, change the behavior of the SDK. + +To instantiate an SDK working as consumer, set two configs on the root of the configuration object, `mode` and `storage`. Set `mode` with the mode of choice, either `'consumer'` or `'consumer_partial'`. Then set `storage` to a valid **storage wrapper** which is the adapter used to connect to the data storage. + +The following shows how to configure and get treatments for a Split SDK instance in consumer or partial consumer mode: + + + +```javascript +import { SplitFactory, PluggableStorage } from '@splitsoftware/splitio-browserjs'; + +var config = { + mode: 'consumer', // Changing the mode to 'consumer' or 'consumer_partial' here + core: { + authorizationKey: '', + key: 'key' + }, + + // Using the PluggableStorage function, to create an storage instance + // with a given a storage wrapper that the SDK should talk to + storage: PluggableStorage({ + // Wrapper objects must implement a special interface + wrapper: MyWrapper, + // Optional prefix to prevent any kind of data collision using + // the same storage with multiple SDKs with different SDK keys + prefix: 'prefix' + }) +}; + +var factory = SplitFactory(config); +var client = factory.client(); + +// Unlike standalone mode, since the storage can by async the SDK will execute its operations asynchronously and +// instead of just returning a treatment as string, it returns a Promise that will be resolved to the treatment value. + +// There are two main options to call getTreatments and extract the treatment. One is the async/await syntax: +var treatment = await client.getTreatment('my-feature-comming-from-storage'); + +// and the other option is just using the returned promise +client.getTreatment('my-feature-comming-from-storage') + .then(treatment => { + // do something with the treatment + }); + +// Same as in standalone mode, you can listen to the SDK events to make sure it's ready before calling for evaluations: + +client.once(client.Event.SDK_READY, function () { + // Depending on the wrapper implementation, this callback will be called immediately or once the connection with the underlying storage is stablished. + // It is recommended to wait for this event before using the SDK. Otherwise the evaluation treatment might be 'control'. +}); + +client.once(client.Event.SDK_READY_TIMED_OUT, function () { + // This callback will be called after the seconds set at the `startup.readyTimeout` config parameter, + // if and only if the SDK_READY event was not emitted for that time. +}); +``` + + + +You can write your own custom storage wrapper for the Split client by extending the IPluggableStorageWrapper interface. + +#### Storage wrapper examples + +We currently maintain storage wrappers for the following technologies: + +- **Cloudflare Durable Objects**: The Durable Object wrapper is available as part of a [template](https://github.com/splitio/cloudflare-workers-template) to help you kick-start a [Cloudflare Workers](https://developers.cloudflare.com/workers/) project. In addition to providing the wrapper, the template demonstrates the basic setup of the Split SDK and Synchronizer. + +- **Vercel Edge Config**: The wrapper is available as an [NPM package](https://npmjs.com/package/@splitsoftware/vercel-integration-utils) and wraps the [Edge Config](https://vercel.com/docs/storage/edge-config) data store. This wrapper is used with the [Split Integration for Vercel](https://vercel.com/integrations/split). Adding the Split integration to a Vercel project sets up the synchronization of the Edge Config data store. See the [Vercel Split integration guide](https://help.split.io/hc/en-us/articles/16469873148173) for more information about the integration setup. + +### User consent + +The SDK allows you to disable the tracking of events and impressions until user consent is explicitly granted or declined. + +The `userConsent` configuration parameter lets you set the initial consent status of the SDK instance, and the factory method `UserConsent.setStatus(boolean)` lets you grant (enable) or decline (disable) dynamic data tracking. + +There are three possible initial states: + * `'GRANTED'`: The user grants consent for tracking events and impressions. The SDK sends them to Split cloud. This is the default value if `userConsent` param is not defined. + * `'DECLINED'`: The user declines consent for tracking events and impressions. The SDK does not send them to Split cloud. + * `'UNKNOWN'`: The user neither grants nor declines consent for tracking events and impressions. The SDK tracks them in its internal storage, and eventually either sends them or not if the consent status is updated to `'GRANTED'` or `'DECLINED'` respectively. + +The status can be updated at any time with the `UserConsent.setStatus` factory method. + +Working with user consent is demonstrated below. + +```javascript title="User consent: Initial config, getter and setter" +var factory = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + // Overwrites the initial consent status of the factory instance, which is 'GRANTED' by default. + // 'UNKNOWN' status represents that the user has neither granted nor declined consent for tracking data, + // so the SDK will locally track data but not send it to Split cloud until consent is changed to 'GRANTED'. + userConsent: 'UNKNOWN' +}); + +// `getStatus` method returns the current consent status. +factory.UserConsent.getStatus() === factory.UserConsent.Status.UNKNOWN; + +// `setStatus` method lets you update the factory consent status at any time. +// Pass `true` for 'GRANTED' and `false` for 'DECLINED'. +factory.UserConsent.setStatus(true); // Consent status changed from 'UNKNOWN' to 'GRANTED'. Data will be sent to Split cloud. +factory.UserConsent.getStatus() === factory.UserConsent.Status.GRANTED; + +factory.UserConsent.setStatus(false); // Consent status changed from 'GRANTED' to 'DECLINED'. Data will not be sent to Split cloud. +factory.UserConsent.getStatus() === factory.UserConsent.Status.DECLINED; +``` + +## Example apps + +The following are example applications detailing how to configure and instantiate the Split JavaScript Browser SDK on commonly used platforms. + +* [Basic HTML](https://github.com/splitio/example-javascript-client) diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/flutter-plugin.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/flutter-plugin.md new file mode 100644 index 00000000000..e1eb5950834 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/flutter-plugin.md @@ -0,0 +1,732 @@ +--- +title: Flutter plugin +sidebar_label: Flutter plugin +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +This guide provides detailed information about our Flutter plugin which is built on top of our [Android SDK](https://help.split.io/hc/en-us/articles/360020343291-Android-SDK) and [iOS](https://help.split.io/hc/en-us/articles/360020401491-iOS-SDK) mobile SDKs. The plugin provides you a way to interact with the native SDKs. + +All of our SDKs are open source. Go to our [Flutter GitHub repository](https://github.com/splitio/flutter-sdk-plugin) to see the source code. + +## Language support + +Dart SDK v2.16.2 and greater, and Flutter v2.5.0 and greater. + +:::warning[Platform support] +This plugin currently supports the Android and iOS platforms. +::: + +## Initialization + +Set up Split in your code base with the following two steps: + +### 1. Add the package in your pubspec.yaml file + +```yaml title="pubspec.yaml" +dependencies: + splitio: 0.2.0 +``` + +### 2. Instantiate the plugin + +```dart title="Flutter" +/// Initialize Split plugin +import 'package:splitio/split_client.dart'; +import 'package:splitio/splitio.dart'; +/// KEY represents your internal user id, or the account id that +/// the user belongs to. +/// This could also be a cookie you generate for anonymous users. +final Splitio _split = Splitio('YOUR_SDK_KEY', 'KEY'); +``` + +We recommend instantiating the `Splitio` object once as a singleton and reusing it throughout your application. + +Configure the plugin with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split UI, on your Admin settings page, API keys section. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. + +## Using the plugin + +### Basic use + +When the SDK is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds, depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while it's in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). + +To make sure the SDK is properly loaded before asking it for a treatment, wait until the SDK is ready, as shown below. You can use the `onReady` parameter when creating the client to get notified when this happens. + +After the observable calls back, you can use the `getTreatment` method to return the proper treatment based on the `FEATURE_FLAG_NAME` and the `key` variable you passed when instantiating the SDK. Then use an if-else-if block as shown below and insert the code for the different treatments that you defined in the Split user interface. Remember the final else branch in your code to handle the client returning control. + +```dart title="Flutter" +/// Get treatment +_split.client(onReady: (client) async { + final String treatment = await client.getTreatment('FEATURE_FLAG_NAME'); + + if (treatment == 'on') { + /// Insert code here to show on treatment + } else if (treatment == 'off') { + /// Insert code here to show off treatment + } else { + /// Insert your control treatment code here + } +}); +``` + +### Attribute syntax + +To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), pass the client's `getTreatment` method as an attribute map at runtime. + +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in the Split user interface to decide whether to show the `on` or `off` treatment to this account. The `getTreatment` method supports five types of attributes: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: + +* **Strings:** Use type String. +* **Numbers:** Use type num (int or double). +* **Dates:** Express the value in milliseconds since epoch.
*Note:* Milliseconds since epoch is expressed in UTC. If your date or date-time combination is in a different timezone, first convert it to UTC, then transform it to milliseconds since epoch. +* **Booleans:** Use type bool. +* **Sets:** Use type List or Set. + +```dart title="Flutter" +final attributes = { + // date attributes are handled as `millis since epoch` + 'registered_date': DateTime.now().millisecondsSinceEpoch, + // this string will be compared against a list called `plan_type` or against another string + 'plan_type': 'growth', + // this number will be compared against a number value called `deal_size` + 'deal_size': 10000, + // this array will be compared against a set called `permissions` + 'permissions': ['read', 'write'] +}; + +final String treatment = + await _client.getTreatment('FEATURE_FLAG_NAME', attributes); + +if (treatment == 'on') { + // insert on code here +} else if (treatment == 'off') { + // insert off code here +} else { + // insert control code here +} +``` + +### Binding attributes to the client + +Attributes can optionally be bound to the client at any time during the SDK lifecycle. These attributes are stored in memory and used in every evaluation to avoid the need to keep the attribute set accessible through the whole app. When an evaluation is called, the attributes provided (if any) at evaluation time are combined with the ones that are already loaded into the SDK memory, with the ones provided at function execution time taking precedence. This enables those attributes to be overridden or hidden for specific evaluations. + +An attribute is considered valid if it follows one of the types listed below: +- String +- Number +- Boolean +- Array + +The SDK validates these before storing them and if there are invalid or missing values, possibly indicating an issue, the methods return the boolean `false` and do not update any value. + +To use these methods, refer to the example below: + +```dart title="Flutter" +var attributes = { + 'registered_date': DateTime.now().millisecondsSinceEpoch, + 'plan_type': 'growth', + 'deal_size': 10000, + 'paying_customer': true, + 'permissions': ['read', 'write'] +}; + +// set attributes returns a future which completes with a true unless there is an issue storing it +var result = await client.setAttributes(attributes); + +// set one attribute and returns a future which completes with a true value unless there is an issue storing it +var result = await client.setAttribute('paying_customer', false); + +// Get an attribute +var planType = await client.getAttribute('plan_type'); + +// Get all attributes +var storedAttributes = await client.getAttributes(); + +// Remove an attribute +var result = await client.removeAttribute('permissions'); + +// Remove all attributes +var result = await client.clearAttributes(); + +``` + +### Multiple evaluations at once + +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` from the Split client to do this. +* `getTreatments`: Pass a list of the feature flag names you want treatments for. +* `getTreatmentsByFlagSet`: Evaluate all flags that are part of the provided set name and are cached on the SDK instance. +* `getTreatmentsByFlagSets`: Evaluate all flags that are part of the provided set names and are cached on the SDK instance. + + + +```dart +const featureFlagNames = ['FEATURE_FLAG_NAME_1', 'FEATURE_FLAG_NAME_2']; +var treatments = {}; + +_split.client(onReady: (client) async { + treatments = await client.getTreatments(featureFlagNames); +}); +``` + + +```dart +var treatments = {}; + +_split.client(onReady: (client) async { + treatments = await client.getTreatmentsByFlagSet('frontend'); +}); +``` + + +```dart +var treatments = {}; + +_split.client(onReady: (client) async { + treatments = await client.getTreatmentsByFlagSets('frontend', 'client_side'); +}); +``` + + + +### Get treatments with configurations + +To [leverage dynamic configurations with your treatments](https://help.split.io/hc/en-us/articles/360026943552), use the `getTreatmentWithConfig` method. + +This method returns an object containing the treatment and associated configuration: + +```dart title="Flutter" +class SplitResult { + final String treatment; + final String? config; +} +``` + +The config element is a stringified version of the configuration JSON defined in the Split user interface. If there is no configuration defined for a treatment, the SDK returns `null` for the config parameter. + +This method taskes the exact set of arguments as the standard `getTreatment` method. See below examples on proper usage: + +```dart title="getTreatmentWithConfig" +SplitResult result = await client.getTreatmentWithConfig('FEATURE_FLAG_NAME'); +var configs = result.config; +var treatment = result.treatment; + +if (treatment == 'on') { + // insert on code here and use configs here as necessary +} else if (treatment == 'off') { + // insert off code here and use configs here as necessary +} else { + // insert control code here +} +``` + +If you need to get multiple evaluations at once, you can also use the `getTreatmentsWithConfig` methods. These methods take the exact same arguments as the [getTreatments](#multiple-evaluations-at-once) methods but return a mapping of feature flag names to `SplitResult` objects instead of strings. Example usage below. + + + +```dart +SplitResult result = await client.getTreatmentsWithConfig('FEATURE_FLAG_NAME'); +var configs = result.config; +var treatment = result.treatment; + +if (treatment == 'on') { + // insert on code here and use configs here as necessary +} else if (treatment == 'off') { + // insert off code here and use configs here as necessary +} else { + // insert control code here +} +``` + + +```dart +SplitResult result = await client.getTreatmentsWithConfigByFlagSet('frontend'); +var configs = result.config; +var treatment = result.treatment; + +if (treatment == 'on') { + // insert on code here and use configs here as necessary +} else if (treatment == 'off') { + // insert off code here and use configs here as necessary +} else { + // insert control code here +} +``` + + +```dart +SplitResult result = await client.getTreatmentsWithConfigByFlagSets(['frontend', 'client_side']); +var configs = result.config; +var treatment = result.treatment; + +if (treatment == 'on') { + // insert on code here and use configs here as necessary +} else if (treatment == 'off') { + // insert off code here and use configs here as necessary +} else { + // insert control code here +} +``` + + + +### Shutdown + +Call the `client.destroy()` method once you've stopped using the client, as this method gracefully shuts down the Split SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. + +```dart title="Flutter" +/// You should call destroy() on the client once it is no longer needed: +_client.destroy(); +``` + +After `destroy()` is called and finishes, any subsequent invocations to `getTreatment`/`getTreatments` or manager methods result in `control` or empty list, respectively. + +## Track + +Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users' actions and metrics. [Learn more about using track events](https://help.split.io/hc/en-us/articles/360020585772) in Split. + +In the examples below, you can see that the `.track()` method can take up to four arguments. The proper data type and syntax for each are: + +* **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: + * Contains 63 characters or fewer. + * Starts with a letter or number. + * Contains only letters, numbers, hyphen, underscore, or period. + * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` +* **TRAFFIC_TYPE:** (Optional) The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **VALUE:** (Optional) The value is used to create the metric. The expected data type is **double**. +* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about [event properties](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties). Split currently supports three types of properties: strings, numbers, and booleans. + +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the client was able to successfully queue the event to be sent back to Split's servers on the next event post. The service returns `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method is provided. + +In the case that a bad input is provided, you can read more about our [SDK's expected behavior](https://help.split.io/hc/en-us/articles/360020585772-Track-events). + +```dart title="Flutter" +_split.client(onReady: (client) async { + /// Named parameters are optional + client.track('EVENT_TYPE', + trafficType: 'TRAFFIC_TYPE', + value: 120.25, + properties: {'package': 'premium', 'admin': true, 'discount': 50}); +}); +``` + +## Configuration + +The SDK has a number of knobs for configuring performance. Each knob is tuned to a reasonable default. However, you can override values when instantiating the `Splitio`: + +```dart title="Flutter" +final SplitConfiguration configurationOptions = SplitConfiguration( + trafficType: 'user', + logLevel: SplitLogLevel.debug, + syncConfig: SyncConfig.flagSets('frontend', 'client_side'), + persistentAttributesEnabled: true); + +final Splitio _split = + Splitio('YOUR_SDK_KEY', 'KEY', configuration: configurationOptions); +``` + +The parameters available for configuration are shown below. + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| featuresRefreshRate | The SDK polls Split servers for changes to feature flags at this rate (in seconds). | 3600 seconds | +| segmentsRefreshRate | The SDK polls Split servers for changes to segments at this rate (in seconds). | 1800 seconds | +| impressionsRefreshRate | The treatment log captures which customer saw what treatment (on, off, etc.) at what time. This log is periodically flushed back to Split servers. This configuration controls how quickly the cache expires after a write (in seconds). | 1800 seconds | +| telemetryRefreshRate | The SDK caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers (in seconds). | 3600 seconds (1 hour) | +| eventsQueueSize | When using `.track`, the number of **events** to be kept in memory. | 10000 | +| eventFlushInterval | When using `.track`, how often is the events queue flushed to Split's servers. | 1800 seconds | +| eventsPerPush | Maximum size of the batch to push events. | 2000 | +| trafficType | When using `.track`, the default traffic type to be used. | not set | +| impressionsQueueSize | Default queue size for impressions. | 30K | +| enableDebug | Enabled verbose mode. | false | +| streamingEnabled | Boolean flag to enable the streaming service as default synchronization mechanism when in foreground. In the event of an issue with streaming, the SDK will fallback to the polling mechanism. If false, the SDK will poll for changes as usual without attempting to use streaming. | true | +| persistentAttributesEnabled | Enables saving attributes on persistent cache which is loaded as part of the SDK_READY_FROM_CACHE flow. All functions that mutate the stored attributes map affect the persistent cache. | false | +| impressionListener | Enables impression listener. If true, generated impressions stream in the impressionsStream() method of Splitio. | false | +| syncConfig | Use it to filter specific feature flags to be synced and evaluated by the SDK. It can be created with the `SyncConfig.flagSets('sets')` method (recommended, flag sets aree available in all tiers) or `SyncConfig(names: ["feature-flag-1", "feature-flag-2"])` for individual names. If not set, all flags are downloaded. | not set | +| syncEnabled | Controls the SDK continuous synchronization flags. When `true`, a running SDK processes the rollout plan updates which is performed in the Split user interface (default). When `false`, it fetches all data upon init, which ensures a consistent experience during a user session and optimizes resources when these updates are not consumed by the app. | true | +| userConsent | User consent status controls the tracking of events and impressions. Possible values are `UserConsent.granted`, `UserConsent.decline`, and `UserConsent.unknown`. See [User consent](#user-consent) for details. | `UserConsent.granted` | +| encryptionEnabled | Enables or disables encryption for cached data. | `false` | +| logLevel | Enables logging according to the level specified. Options are `SplitLogLevel.none`, `SplitLogLevel.verbose`, `SplitLogLevel.debug`, `SplitLogLevel.info`, `SplitLogLevel.warning`, and `SplitLogLevel.error`. | `SplitLogLevel.none` | +| impressionsMode | This configuration defines how impressions (decisioning events) are queued. Supported modes are `ImpressionsMode.optimized`, `ImpressionsMode.none`, and `ImpressionsMode.debug`. In `ImpressionsMode.optimized` mode, only unique impressions are queued and posted to Split; this is the recommended mode for experimentation use cases. In `ImpressionsMode.none` mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use `ImpressionsMode.none` when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In `ImpressionsMode.debug` mode, ALL impressions are queued and sent to Split. This is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. | `ImpressionsMode.optimized` | +| readyTimeout | Maximum amount of time (in seconds) to wait until the `onTimeout` callback is fired or `whenTimeout` future is completed. A negative value means no timeout. | 10 seconds | +| certificatePinningConfiguration | If set, enables certificate pinning for the given domains. For details, see the [Certificate pinning](#certificate-pinning) section below. | null | + +## Manager + +Use these methods on Splitio instance to get a list of the feature flags available to the Split client. + +```dart title="Flutter" +/// Retrieves the feature flags that are currently registered with the SDK. +Future> splits(); + +/// Returns the feature flags registered with the SDK of this name. +Future split(String splitName); + +/// Returns the names of feature flags registered with the SDK. +Future> splitNames(); +``` + +The `SplitView` class referenced above has the following structure: + +```dart title="Flutter" +class SplitView { + String name; + String trafficType; + bool killed = false; + List treatments = []; + int changeNumber; + Map configs = {}; + List sets = []; + String defaultTreatment; +} +``` + +## Listener + +Split SDKs send impression data back to Split servers periodically as a result of evaluating feature flags. To additionally send this information to a location of your choice, use the `impressionsStream`. + +This provides a stream that publishes `Impression` objects every time one is generated. + +```dart title="Flutter" +final Splitio _split = Splitio(_apiKey, _matchingKey, + configuration: SplitConfiguration( + trafficType: "user", + )); + +StreamSubscription impressionsStream = _split.impressionsStream().listen((impression) { + /// Fired each time an impression has been generated. +}); +``` + +The `Impression` class has the following format. + +```dart title="Flutter" +class Impression { + final String? key; + final String? bucketingKey; + final String? split; + final String? treatment; + final num? time; + final String? appliedRule; + final num? changeNumber; + final Map attributes; +} +``` + +An impression listener is called asynchronously from the corresponding evaluation, but is almost immediate. + +## Logging + +To enable logging, the `logLevel` setting is available in the configuration class: + +```dart title="Setup logs" +final Splitio _split = Splitio(_sdkKey, _matchingKey, + configuration: SplitConfiguration( + logLevel: SplitLogLevel.debug, + )); +``` + +## Advanced use cases + +This section describes advanced use cases and features provided by the SDK. + +### Instantiate multiple SDK clients + +Split supports the ability to release based on multiple traffic types. For example, with traffic types, you can release to `users` in one feature flag and `accounts` in another. If you are unfamiliar with using multiple traffic types, refer to the [Traffic type guide](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) for more information. + +Each SDK client is tied to one specific customer ID at a time, so if you need to roll out feature flags by different keys, instantiate multiple SDK clients, one for each traffic type. For example, you may want to roll out the feature `USER_POLL` by `users` and the feature `ACCOUNT_PERMISSIONING` by `accounts`. You can do this with the example below: + +```dart title="Flutter" +final Splitio _split = Splitio('YOUR_SDK_KEY', 'ACCOUNT_ID'); + +/// Create a client for the default key, in this case, the account id. +final SplitClient _accountClient = _split.client(onReady: (client) async { + var userPollTreatment = client.getTreatment('USER_POLL'); +}); + +/// To create another client for a user instead, just pass in a +/// User ID to the splitService.initClient() method. (This is only valid after +/// at least one client has been initialized). +final SplitClient _userClient = _split.client( + matchingKey: 'USER_ID', + onReady: (client) async { + var accountPermissioningTreatment = + client.getTreatment('ACCOUNT_PERMISSIONING'); + }); + +/// Track events for accounts +_userClient.track('PAGELOAD', value: 7.86); + +/// Track events for users +_accountClient.track('ACCOUNT_CREATED'); +``` + +:::info[Number of SDK instances] +While the SDK does not put any limitations on the number of instances that can be created, we strongly recommend keeping the number of clients down to **one** or **two**. +::: + +### Subscribe to events + +You can subscribe to four different callbacks when creating a client. + +* `onReadyFromCache`. This event fires once the SDK is ready to evaluate treatments using a locally cached version of your rollout plan from a previous session (which might be stale). If there is data in the cache, this event fires almost immediately, since access to the cache is fast; otherwise, it doesn't fire. +* `onReady`. This event fires once the SDK is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Split servers. +* `onTimeout`. This event fires if there is no cached version of your rollout plan in disk cache, and the SDK could not fully download the data from Split servers within the time specified by the `readyTimeout` configuration parameter. This event does not indicate that the SDK initialization was interrupted. The SDK continues downloading the rollout plan and fires the `onReady` event when finished. This delayed `onReady` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. +* `onUpdate`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in the Split user interface. + +```dart title="Flutter" +final Splitio _split = Splitio('YOUR_SDK_KEY', 'ACCOUNT_ID'); + +_split.client(onReady: (client) { + /// Client has fetched the most up-to-date definitions. +}, onReadyFromCache: (client) { + /// Fired after the SDK could confirm the presence of the Split data. + /// This event fires really quickly, since there's no actual fetching of information. + /// Keep in mind that data might be stale, this is NOT a replacement of sdkReady. +}, onUpdated: (client) { + /// Fired each time the client state changes, for example, + /// when a feature flag or a segment changes. +}, onTimeout: (client) { + /// Fired if the client was not able to be ready. + /// GetTreatment can still be called but the result may be CONTROL. +}); +``` + +You can also receive Futures (or a Stream, for the Update event) by accessing the following methods in the client. + +```dart title="Flutter" +_client.whenReady().then((client) { + /// Client has fetched the most up-to-date definitions. +}); + +_client.whenReadyFromCache((client) { + /// Fired after the SDK could confirm the presence of the Split data. + /// This event fires really quickly, since there's no actual fetching of information. + /// Keep in mind that data might be stale, this is NOT a replacement of sdkReady. +}); + +StreamSubscription streamSubscription = _client.whenUpdated().listen((client) { + /// Fired each time the client state changes, for example, + /// when a feature flag or a segment changes. +}); + +_client.whenTimeout().then((client) { + /// Fired if the client was not able to be ready. + /// GetTreatment can still be called but the result may be CONTROL. +}); +``` + +### User consent + +The plugin allows you to disable the tracking of events and impressions until user consent is explicitly granted or declined. + +The `userConsent` configuration parameter lets you set the initial consent status of the SDK, and the `Splitio` method `setUserConsent(enabled: bool)` lets you grant (enable) or decline (disable) the dynamic data tracking. + +The following are the three possible initial states: + + * `UserConsent.granted`. The user grants consent for tracking events and impressions. The SDK sends them to the Split cloud. This is the default value if the `userConsent` param is not defined. + * `UserConsent.declined`. The user declines consent for tracking events and impressions. The SDK does not send them to the Split cloud. + * `UserConsent.unknown`. The user neither grants nor declines consent for tracking events and impressions. The SDK tracks them in its internal storage, and eventually either sends them or not if the consent status is updated to `'GRANTED'` or `'DECLINED'` respectively. The status can be updated at any time with the `setUserConsent` factory method. + +```dart title="User consent: initial config, getter and setter" + // Overwrites the initial consent status of the factory instance, which is 'GRANTED' by default. + // 'UNKNOWN' status represents that the user has neither granted nor declined consent for tracking data, + // so the SDK locally tracks data but not send it to Split cloud until consent is changed to 'GRANTED'. + + final Splitio _split = Splitio(_sdkKey, _matchingKey, + configuration: SplitConfiguration( + userConsent: UserConsent.unknown, + )); + + // Changed User Consent status to 'GRANTED'. Data will be sent to Split cloud. + _split.setUserConsent(true); + // Changed User Consent status to 'DECLINED'. Data will not be sent to Split cloud. + _split.setUserConsent(false); + + // The 'getUserConsent' method returns User Consent status. + // We expose the constants for customer checks and tracking. + + UserConsent userConsent = await _split.getUserConsent(); + if (userConsent == UserConsent.declined) { + print("USER CONSENT DECLINED"); + } + + if (userConsent == UserConsent.granted) { + print("USER CONSENT GRANTED"); + } + + if (userConsent == UserConsent.unknown) { + print("USER CONSENT UNKNOWN"); + } +``` + +### Certificate pinning + +The plugin allows you to constrain the certificates that it trusts, by pinning a certificate's `SubjectPublicKeyInfo` providing the public key as a ___base64 SHA-256___ hash or a ___base64 SHA-1___ hash. + +Each pin corresponds to a host. For subdomains, you can optionally use wildcards, where `*` will match one subdomain (e.g. `*.example.com`), and `**` will match any number of subdomains (e.g `**.example.com`). + +To set the plugin to require pinned certificates for specific hosts, add the `CertificatePinningConfiguration` object to the configuration, as shown below. + +```dart title="Flutter" + // Define pins for certificate pinning + final CertificatePinningConfiguration pinningConfig = CertificatePinningConfiguration() + + // Provide a base 64 SHA-256 hash + .addPin("*.example1.com", "sha256/7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y="); + + // Set the CertificatePinningConfiguration property for the Split client configuration + SplitConfiguration config = SplitConfiguration( + certificatePinningConfiguration: pinningConfig); + +``` + +### Link with native factory + +A native Split Factory instance can be shared with the plugin to save resources when evaluations need to be performed on native platform logic. To do so, do the following: + +#### Android + +1. If not created already, create a subclass of Android's `Application`, and add its name to the Manifest. + +```java title="Android" +public class CustomApplication extends Application { + +} +``` + +```xml title="AndroidManifest.xml" + +``` + +2. Add the Split Android SDK dependency to your project's `build.gradle` file. + +```groovy title="Gradle" +dependencies { + implementation 'io.split.client:android-client:split_version' + ... +} +``` + +3. Create a property in your subclass of `Application` to hold your factory instance. +4. Initialize the factory in the `onCreate` callback of your `Application` subclass. + +```java title="Android" +public class CustomApplication extends Application { + private SplitFactory factory; + + @Override + public void onCreate() { + super.onCreate(); + + try { + factory = SplitFactoryBuilder + .build("YOUR_SDK_KEY", + new Key("USER_KEY"), + SplitClientConfig.builder() + .build(), + getApplicationContext()); + } catch (Exception ex) { + ex.printStackTrace(); + } + } +} +``` + +5. Make the `Application` subclass implement the `SplitFactoryProvider` interface, and return the previously created factory in the overridden `getSplitFactory()` method. +```java title="Android" +public class CustomApplication extends Application implements SplitFactoryProvider { + private SplitFactory factory; + + @Override + public void onCreate() { + ... + } + + @Override + public SplitFactory getSplitFactory() { + return factory; + } +} +``` + +#### iOS + +1. Add the Split iOS SDK dependency to your app's `Podfile`. + +```podfile title="Podfile" + pod 'Split', '~> 2.15.0' +... +``` + +2. Add a property in your AppDelegate class to hold the factory instance. Make sure to import `Split`. + +```swift title="iOS" +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + + private var splitFactory: SplitFactory? + + ... +} +``` + +3. Initialize the factory just before the `GeneratedPluginRegistrant.register(with: self)` line. + +```swift title="iOS" +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + + private var splitFactory: SplitFactory? + + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + let config = SplitClientConfig() + splitFactory = DefaultSplitFactoryBuilder() + .setConfig(config) + .setApiKey("YOUR_SDK_KEY") + .setKey(Key(matchingKey: "USER_KEY")) + .build() + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + ... +} +``` + +4. Implement the `SplitFactoryProvider` protocol in your `AppDelegate` and return the previously created factory in the overridden `getFactory()` method. + +```swift title="iOS" +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate, SplitFactorProvider { + + private var splitFactory: SplitFactory? + + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + ... + } + + func getFactory() -> SplitFactory? { + splitFactory + } +} +``` + +:::warning[Warning] +By using this method, all configuration declared when instantiating the Plugin in Flutter are ignored, since the factory is already instantiated and its configuration loaded. + +Instantiating the factory natively prevents the plugin from setting up an Impression Listener, so impressions won't be accessible from Flutter. However, Impression Listeners can still be added and used in native code. +::: \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/ios-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/ios-sdk.md new file mode 100644 index 00000000000..aa8241101b0 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/ios-sdk.md @@ -0,0 +1,894 @@ +--- +title: iOS SDK +sidebar_label: iOS SDK +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +This guide provides detailed information about our iOS SDK. All of our SDKs are open source. Go to our [iOS SDK GitHub repository](https://github.com/splitio/ios-client) to see the source code. + +## Language support + +This library is compatible with iOS and tvOS deployment target versions 9.0+, macOS 10.11+, and watchOS 7.0+. Xcode 12 and later is also required, but we recommend the minimum version necessary to publish apps on the [AppStore](https://developer.apple.com/news/?id=ib31uj1j). + +## Initialization + +To get started, set up Split in your code base with the two following steps. + +### 1. Import the SDK into your project + +#### Swift Package Manager + +You can import the SDK in your project by using Swift Package Manager. This can be done through XCode or by editing manually the **Package.swift** file to add the iOS SDK repository as a dependency. + +#### CocoaPods + +You can also import the SDK into your Xcode project using CocoaPods, adding it in your **Podfile**. + +```swift title="Podfile" +pod 'Split', '~> 3.1.1' +``` + +#### Carthage + +This is another option to import the SDK. Just add it in your **Cartfile**. + +```swift title="Cartfile" +github "splitio/ios-client" 3.1.1 +``` + +Once added, follow the steps provided in the [Carthage Readme](https://github.com/Carthage/Carthage/blob/master/README.md#if-youre-building-for-ios-tvos-or-watchos). + +### 2. Instantiate the SDK and create a new Split client + +The first time that the SDK is instantiated, it starts background tasks to update an in-memory cache and in-storage cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds depending on the size of the data. + +If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while it is in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). + +After the first initialization, the fetched data is stored. Further initializations fetch data from that cache and the configuration is available immediately. + +We recommend instantiating the Split factory once as a singleton and reusing it throughout your application. + +Configure the SDK with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split UI, on your Admin settings page, API keys section. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. + +```swift title="Swift" +import Split + +// Your Split SDK Key +let sdkKey: String = "YOUR_SDK_KEY" + +//User Key +// key represents your internal user id, or the account id that +// the user belongs to. +// This could also be a UUID you generate for anonymous users. +let key: Key = Key(matchingKey: "key") + +//Split Configuration +let config = SplitClientConfig() + +//Split Factory +let builder = DefaultSplitFactoryBuilder() +let factory = builder.setApiKey(sdkKey).setKey(key).setConfig(config).build() + +//Split Client +let client = factory?.client +``` + +## Using the SDK + +### Basic use + +To make sure the SDK is properly loaded before asking it for a treatment, wait until the SDK is ready as shown below. We set the client to listen for the `sdkReady` event triggered by the SDK before asking for an evaluation. + +Once the `sdkReady` event fires, you can use the `getTreatment` method to return the proper treatment based on the feature flag name you pass and the key you passed when instantiating the SDK. + +From there, you need to use an if-else-if block as shown below and plug the code in for the different treatments that you defined in the Split user interface. Make sure to remember the final else branch in your code to handle the client returning control. + +```swift title="Swift" +client?.on(event: SplitEvent.sdkReady) { + // Evaluate feature flag in Split + let treatment = client?.getTreatment("FEATURE_FLAG_NAME") + + if treatment == "on" { + // insert code here to show on treatment + } else if treatment == "off" { + // insert code here to show off treatment + } else { + // insert your control treatment code here + } +} +client?.on(event: SplitEvent.sdkReadyTimedOut) { + //handle for timeouts here + print("SDK time out") +} +``` + +Also, a `sdkReadyFromCache` event is available, which allows you to be aware of when the SDK has loaded data from cache. This way it is ready to evaluate feature flags using those locally cached definitions. + +```swift title="Swift" +client?.on(event: SplitEvent.sdkReadyFromCache) { + // Evaluate feature flag in Split + let treatment = client?.getTreatment("FEATURE_FLAG_NAME") + + if treatment == "on" { + // insert code here to show on treatment + } else if treatment == "off" { + // insert code here to show off treatment + } else { + // insert your control treatment code here + } +} +``` + +Starting from version 2.24.5, it is possible to configure the handler to run in a background thread or specify a custom queue. + + + +```swift +client?.on(event: SplitEvent.sdkReadyFromCache, runInBackground: true) { + // Handler code +} +``` + + +```swift +let customQueue = DispatchQueue(label: "custom-queue") +client?.on(event: SplitEvent.sdkReadyFromCache, queue: customQueue) { + // Handler code +} +``` + + + + +### Attribute syntax + +To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK's `getTreatment` method needs to be passed an attribute map at runtime. + +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the Rollout plan as defined in the Split user interface to decide whether to show the `on` or `off` treatment to this account. + +The `getTreatment` method supports five types of attributes: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: + +* **Strings:** Use type String. +* **Numbers:** Use type `Int64`. +* **Dates:** Use the value `TimeInterval`. For instance, the value for the `registered_date` attribute below is `Date().timeIntervalSince1970`, which is a `TimeInterval` value. +* **Booleans:** Use type `Bool`. +* **Sets:** Use type `[String]`. + +```swift title="Swift" +var attributes: [String:Any] = [:] + +attributes["plan_type"] = "growth" +attributes["registered_date"] = Date().timeIntervalSince1970 +attributes["deal_size"] = 1000 +attributes["paying_customer"] = true +let perms: [String] = ["read", "write"]; +attributes["permissions"] = perms + +// See client initialization above +let treatment = client?.getTreatment("FEATURE_FLAG_NAME", attributes: attributes) + +if treatment == "on" { + // insert code here to show on treatment +} else if treatment == "off" { + // insert code here to show off treatment +} else { + // insert your control treatment code here +} +``` + +### Binding attributes to the client + +Attributes can be bound to the client at any time during the SDK lifecycle. These attributes will be stored in memory and used in every evaluation to avoid the need for keeping the attribute set accessible through the whole app. These attributes can be cached into the persistent caching mechanism of the SDK making them available for future sessions, as well as part of the SDK_READY_FROM_CACHE flow by setting the `persistentAttributesEnabled` to true. No need to wait for your attributes to be loaded at every session before evaluating flags that use them. + +When an evaluation is called, the attributes provided (if any) at evaluation time are combined with the ones already loaded into the SDK memory, with the ones provided at function execution time take precedence, enabling for those attributes to be overridden or hidden for specific evaluations. + +An attribute is considered valid if it follows one of the types listed below: +- String +- Number +- Boolean +- Array + +The SDK validates these before storing them and if there are invalid or missing values, possibly indicating an issue, the methods return the boolean `false` and do not update any value. + +The snippet below shows how to update these attributes: + +```Swift +@objc public protocol SplitClient { + + /** + Set one single attribute and returns true unless there is an issue storing it. + If `persistentAttributesEnabled` config is enabled, the attribute is also written to the persistent cache. + */ + func setAttribute(name: String, value: Any) -> Bool + + /** + Retrieves the value of a given attribute stored in cache. + */ + func getAttribute(name: String) -> Any? + + /** + Set multiple attributes and returns true unless there is an issue storing them. + If `persistentAttributesEnabled` config is enabled, the attributes are also written to the persistent cache. + */ + func setAttributes(_ values: [String: Any]) -> Bool + + /** + Retrieves a Map with the values of all attributes stored in cache. + */ + func getAttributes() -> [String: Any]? + + /** + Remove one single attribute and returns true unless there is an issue deleting it. + If `persistentAttributesEnabled` config is enabled, the attribute is also deleted from the persistent cache and won't be available in a subsequent session. + */ + func removeAttribute(name: String) -> Bool + + /** + Clear the whole attribute cache and return true unless there is an issue with the operation and some attributes might still be cached. + If `persistentAttributesEnabled` config is enabled, the attributes are also deleted from the persistent cache and won't be available in a subsequent session. + */ + func clearAttributes() -> Bool +} +``` + +```swift title="Swift" +// Prepare a Map with several attributes +var attributes: [String:Any] = [:] +attributes["plan_type"] = "growth" +attributes["registered_date"] = Date().timeIntervalSince1970 +attributes["deal_size"] = 1000 + +// Now set these on the client +let result = client.setAttributes(attributes) + +// Set one attribute +let result = client.setAttribute(name: "registered_date", value: Date().timeIntervalSince1970) + +// Get an attribute +let result = client.getAttribute(name: "registered_date") + +// Get all attributes +let result = client.getAttributes() + +// Remove an attribute +let result = client.removeAttribute(name: "deal_size") + +// Remove all attributes +let result = client.clearAttributes() +``` + +### Multiple evaluations at once + +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` method of the Split client to do this. +* `getTreatments`: Pass a list of the feature flag names you want treatments for. +* `getTreatmentsByFlagSet`: Evaluate all flags that are part of the provided set name and are cached on the SDK instance. +* `getTreatmentsByFlagSets`: Evaluate all flags that are part of the provided set names and are cached on the SDK instance. + +```swift title="Swift" +// Assuming client is an instance of a class that has these methods +let featureFlagNames = ["FEATURE_FLAG_NAME_1", "FEATURE_FLAG_NAME_2"] +let treatments = client.getTreatments(splits: featureFlagNames, attributes: nil) + +let treatmentsByFlagSet = client.getTreatmentsByFlagSet("frontend", attributes: nil) + +let flagSets = ["frontend", "client_side"] +let treatmentsByFlagSets = client.getTreatmentsByFlagSets(flagSets, attributes: nil) + +// Treatments will have the following form: +// [ +// "FEATURE_FLAG_NAME_1": "on", +// "FEATURE_FLAG_NAME_2": "visa" +// ] + +// Treatments will have the following form: +// [ +// "FEATURE_FLAG_NAME_1": "on", +// "FEATURE_FLAG_NAME_2": "visa" +// ] +``` + +### Get treatments with configurations + +To [leverage dynamic configurations with your treatments](https://help.split.io/hc/en-us/articles/360026943552), use the `getTreatmentWithConfig` methods. These methods returns an object containing the treatment and associated configuration. + +The config element is a stringified version of the configuration JSON defined in the Split user interface. If there is no configuration defined for a treatment, the SDK returns `null` for the config parameter. + +This method takes the exact same set of arguments as the standard `getTreatment` method. See below for examples on proper usage: + +```swift title="Swift" +let result = client.getTreatmentWithConfig("new_boxes", attributes: attributes) +let config = try? JSONSerialization.jsonObject(with: result.config.data(using: .utf8)!, options: []) as? [String: Any] +let treatment = result.treatment +``` + +If you need to get multiple evaluations at once, you can also use `getTreatmentsWithConfig` methods. These methods take the exact same arguments as the [getTreatments](#multiple-evaluations-at-once) methods but return a mapping of feature flag names to splitResults instead of strings. Refer to the example below. + +```swift title="Swift" +let featureFlagList = ["FEATURE_FLAG_NAME_1", "FEATURE_FLAG_NAME_2", "FEATURE_FLAG_NAME_3"] +let treatments = client?.getTreatmentsWithConfig(splits: featureFlagList, attributes: nil) + +let treatmentsByFlagSet = client.getTreatmentsWithConfigByFlagSet("frontend", attributes: nil) + +let flagSets = ["frontend", "client_side"] +let treatmentsByFlagSets = client.getTreatmentsWithConfigByFlagSets(flagSets, attributes: nil) + +// treatments will have the following form: +// { +// "FEATURE_FLAG_NAME_1": { "treatment": "on", "config": "{ \"color\":\"red\" }"}, +// "FEATURE_FLAG_NAME_2": { "treatment": "visa", "config": "{ \"color\":\"red\" }"} +// } +``` + +### Shutdown + +Before letting your app shut down, call `destroy()` as it gracefully shuts down the Split SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions and events. + +```swift title="Swift" +client?.destroy() +``` + +Also, this method has a completion closure which can be used to run some code after destroy was executed. For instance, the following snippet waits until destroy has finished to continue execution: + +```swift title="Swift" +let semaphore = DispatchSemaphore(value: 0) +client?.destroy(completion: { + _ = semaphore.signal() +}) +semaphore.wait() +``` + +After `destroy()` is called, any subsequent invocations to the `client.getTreatment()` or `manager` methods result in `control` or empty list respectively. + +:::warning[Important!] +A call to the `destroy()` method also destroys the factory object. When creating new client instance, first create a new factory instance. +::: + +## Track + +Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users’ actions and metrics. + +[Learn more](https://help.split.io/hc/en-us/articles/360020585772) about using track events in feature flags. + +In the examples below, you can see that the `.track()` method can take up to four arguments. The proper data type and syntax for each are: + +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: + * Contains 63 characters or fewer. + * Starts with a letter or number. + * Contains only letters, numbers, hyphen, underscore, or period. + * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}`. +* **VALUE:** (Optional) The value used in creating the metric. This field can be sent in as null or 0 if you intend to only use the count function when creating a metric. The expected data type is **Double**. +* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. + +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event sent back to Split's servers on the next event post. The SDK returns `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method is provided. + +In case a bad input is provided, you can read more about our SDK's expected behavior in our [Events](https://help.split.io/hc/en-us/articles/360020585772-Track-events) guide. + +```swift title="Swift" +// Event without a value +let resp = client?.track(trafficType: "TRAFFIC_TYPE", eventType: "EVENT-TYPE") +// Example +let resp = client?.track(trafficType: "user", eventType: "page_load_time") + +// If you would like to associate a value to an event +let resp = client?.track(trafficType: "TRAFFIC_TYPE", eventType: "EVENT-TYPE", value: VALUE) +// Example +let resp = client?.track(trafficType: "user", eventType: "page_load_time", value: 83.334) + +// If you would like to associate just properties to an event +let resp = client?.track(trafficType: "TRAFFIC_TYPE", eventType: "EVENT-TYPE", properties: PROPERTIES) +// Example +let properties: [String:Any] = ["package": "premium", "discount": 50, "admin": true] +let resp = client?.track(trafficType: "user", eventType: "page_load_time", properties: properties) + +// If you would like to send an event but you've already defined the traffic type in the config of the client +let resp = client?.track(eventType: "EVENT-TYPE") +// Example +let resp = client?.track(eventType: "page_load_time") + +// If you would like to associate a value to an event and you've already defined the traffic type in the config of the client +let resp = client.track(eventType: "EVENT-TYPE", value: VALUE) +// Example +let resp = client?.track(eventType: "page_load_time", value: 83.334) + +// If you would like to associate properties to an event and you've already defined the traffic type in the config of the client +let resp = client.track(eventType: "EVENT-TYPE", properties: PROPERTIES) +// Example +let properties: [String:Any] = ["package": "premium", "discount": 50, "admin": true] +let resp = client?.track(eventType: "page_load_time", proerties: properties) +``` + +## Manager + +Use the Split Manager to get a list of feature flags available to the Split client. + +To instantiate a Manager in your code base, use the same factory that you used for your client. + +```swift title="Swift" +let apiKey: String = "YOUR_API_KEY" +let key: Key = Key(matchingKey: "key") +let config = SplitClientConfig() +let builder = DefaultSplitFactoryBuilder() +let factory = +builder.setApiKey(apiKey).setKey(key).setConfig(config).build() +let manager = factory?.manager +``` + +The Manager then has the following properties and methods available. + +```swift title="Swift" +/** + * Retrieves the feature flags that are currently registered with the + * SDK. + * + * @return an array of SplitView or empty. + */ +var splits: [SplitView] { get } + +/** + * Returns the names of feature flags registered with the SDK. + * + * @return an array of String (feature flag names) or empty + */ +var splitNames: [String] { get } + +/** + * Returns the feature flag registered with the SDK of this name. + * + * @return SplitView or nil + */ +func split(featureName: String) -> SplitView? +``` + +The `SplitView` class referenced above has the following structure. + +```swift title="Swift" +public class SplitView: NSObject, Codable { + + @objc public var name: String? + @objc public var trafficType: String? + @objc public var defaultTreatment: String? + public var killed: Bool? + @objc public var isKilled: Bool { + return killed ?? false + } + @objc public var treatments: [String]? + @objc public var sets: [String]? + public var changeNumber: Int64? + + @objc public var changeNum: NSNumber? { + return changeNumber as NSNumber? + } + @objc public var configs: [String: String]? + @objc public var impressionsDisabled: Bool = false + +} +``` + +## Configuration + +The SDK has a number of knobs for configuring performance. Each knob is tuned to a reasonable default. However, you can override the value while instantiating the SDK. The parameters available for configuration are shown below. + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| featuresRefreshRate | The SDK polls Split servers for changes to feature flags at this rate (in seconds). | 3600 seconds (1 hour) | +| segmentsRefreshRate | The SDK polls Split servers for changes to segments at this rate (in seconds). | 1800 seconds (30 minutes) | +| impressionRefreshRate | The treatment log captures which customer saw what treatment (on, off, etc.) at what time. This log is periodically flushed back to Split servers. This configuration controls how quickly the cache expires after a write (in seconds). | 1800 seconds (30 minutes) | +| impressionsQueueSize | Default queue size for impressions. | 30K | +| eventsPushRate | When using `.track`, how often the events queue is flushed to Split servers. | 1800 seconds| +| eventsPerPush | Maximum size of the batch to push events. | 2000 | +| eventsFirstPushWindow | Amount of time to wait for the first flush. | 10 seconds | +| eventsQueueSize | When using `.track`, the number of **events** to be kept in memory. | 10000 | +| trafficType | (optional) The default traffic type for events tracked using the `track` method. If not specified, every `track` call should specify a traffic type. | not set | +| telemetryRefreshRate | The SDK caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers (in seconds). | 3600 seconds (1 hour) | +| logLevel | Enables logging according to the level specified. Options are `NONE`, `VERBOSE`, `DEBUG`, `INFO`, `WARNING`, and `ERROR`. | `NONE` | +| synchronizeInBackground | Activates synchronization when application host is in background. | false | +| streamingEnabled | Boolean flag to enable the streaming service as default synchronization mechanism when in foreground. In the event of an issue with streaming, the SDK falls back to the polling mechanism. If false, the SDK polls for changes as usual without attempting to use streaming. | true | +| sync | Optional SyncConfig instance. Use it to filter specific feature flags to be synced and evaluated by the SDK. These filters can be created with the `SplitFilter::bySet` static function (recommended, flag sets are available in all tiers), or `SplitFilter::byName` static function, and appended to this config using the `SyncConfig` builder. If not set or empty, all feature flags are downloaded by the SDK. | null | +| offlineRefreshRate | The SDK periodically reloads the localhost mocked feature flags at this given rate in seconds. This can be turned off by setting it to -1 instead of a positive number. | -1 (off) | +| sdkReadyTimeOut | Amount of time in milliseconds to wait before notifying a timeout. | -1 (not set) | +| persistentAttributesEnabled | Enables saving attributes on persistent cache which is loaded as part of the SDK_READY_FROM_CACHE flow. All functions that mutate the stored attributes map affect the persistent cache.| false | +| syncEnabled | Controls the SDK continuous synchronization flags. When `true`, a running SDK processes rollout plan updates performed in the Split user interface (default). When `false`, it fetches all data upon init, which ensures a consistent experience during a user session and optimizes resources when these updates are not consumed by the app. | true | +| impressionsMode | This configuration defines how impressions (decisioning events) are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Split; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, ALL impressions are queued and sent to Split; this is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. | `OPTIMIZED` | +| userConsent | User consent status used to control the tracking of events and impressions. Possible values are `GRANTED`, `DECLINED`, and `UNKNOWN`. See the [User consent](#user-consent) section for details. | `GRANTED` | +| encryptionEnabled | Enables or disables encryption for cached data. | `false` | +| httpsAuthenticator | If set, the SDK uses it to authenticate network requests. To set this value, an implementation of SplitHttpAuthenticator must be provided. | `nil` | +| prefix | Allows to use a prefix when naming the SDK storage. Use this when using multiple `SplitFactory` instances with the same SDK key. | `nil` | +| certificatePinningConfig | If set, enables certificate pinning for the given domains. For details, see the [Certificate pinning](#certificate-pinning) section below. | null | + +To set each of the parameters defined above, use the syntax below: + +```swift title="Swift" +import Split + +// Your Split SDK key +let sdkKey: String = "YOUR_SDK_KEY" + +//User Key +let key: Key = Key(matchingKey: "key") + +//Split Configuration +let config = SplitClientConfig() +config.impressionRefreshRate = 30 +config.isDebugModeEnabled = false +let syncConfig = SyncConfig.builder() + .addSplitFilter(SplitFilter.bySet(["frontend"])) + .build() +config.sync = syncConfig + +//Split Factory +let builder = DefaultSplitFactoryBuilder() +let factory = +builder.setApiKey(sdkKey).setKey(key).setConfig(config).build() + +//Split Client +let client = factory?.client +``` + +## Localhost mode + +For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the Split SDK can be started in 'localhost' mode. In this mode, the SDK neither polls nor updates Split servers, rather it uses an in-memory data structure to determine what treatments to show to the customer for each of the features. + +To use the SDK in localhost mode, replace the API Key with "localhost", as shown in the example below: + +Since version 2.1.0, our SDK supports a new type of localhost feature flag definition file using the YAML format. This new format allows the user to map different keys to different treatments within a single feature flag, and add configurations to them. This file must be included into the project bundle and it is used as an initial file. It is copied to the cache folder, then it can be edited while app is running to simulate feature flag changes. When no file is added to the app bundle, an error occurs. The file periodically reloads. This period can be updated through the offlineRefreshRate config. Also, the refresh process can be turned off by setting this config to -1. + +The new format is a list of single-key maps (one per mapping feature_flag-keys-config), defined as follows: + +```yaml title="YAML" +- my_feature: + treatment: "on" + keys: "key" + config: "{\"desc\" : \"this applies only to ON treatment\"}" +- some_other_feature: + treatment: "off" +- my_feature: + treatment: "off" +- other_feature: + treatment: "off" + keys: ["key_1", "key_2"] + config: "{\"desc\" : \"this overrides multiple keys and returns off treatment for those keys\"}" +``` + +In the example above, we have four entries: + * The first entry defines that for feature flag `my_feature`, the key `key` returns the treatment `on` and the `on` treatment is tied to the configuration `{"desc" : "this applies only to ON treatment"}`. + * The second entry defines that the feature flag `some_other_feature` will always return the `off` treatment and no configuration. + * The third entry defines that `my_feature` always returns `off` for all keys that don't match another entry. In this case, any key other than `key`. + * The fourth entry shows how an example to override a treatment for a set of keys. + +You can set the name of the Split localhost YAML file within cache folder as shown in the example below: + +```swift title="Swift" +// Split SDK key must be "localhost" +let apiKey: String = "localhost" +let key: Key = Key(matchingKey: "key") +let config = SplitClientConfig() +config.splitFile = "localhost.yaml" +let builder = DefaultSplitFactoryBuilder() +self.factory = +builder.setApiKey("localhost").setKey(key).setConfig(config).build() +``` + +If SplitClientConfig.splitFile is not set, Split SDK maintains backward compatibility by trying to load the legacy file (.splits), now deprecated. In this mode, the SDK loads a local file called *localhost.splits* which has the following line format: + +Starting from version 2.24.2, it is possible to update feature flag definitions programmatically by using the Localhost factory's `updateLocalhost` method, as shown below. + +```swift title="Swift" +// Split SDK key must be "localhost" +let apiKey: String = "localhost" +let key: Key = Key(matchingKey: "key") +let config = SplitClientConfig() +let builder = DefaultSplitFactoryBuilder() +self.factory = builder.setApiKey("localhost").setKey(key).setConfig(config).build() + +// SplitLocalhostDataSource protocol declares the updating methods +if guard let datasource = self.factory as? SplitLocalhostDataSource else { return } + +// Yalm file content +datasource.updateLocalhost(yaml: yaml_content) + +// Split file content +datasource.updateLocalhost(splits: splits_content) +``` + +FEATURE_FLAG_NAME TREATMENT + +Additionally, you can include comments to the file starting a line with the ## character. + +**Example:** A sample *localhost.splits* file + +```bash title="Shell" + ## This line is a comment + ## Following line has feature flag = FEATURE_ONE and treatment = ON + FEATURE_ONE ON + FEATURE_TWO OFF + ## Previous line has feature flag = FEATURE_TWO, treatment = OFF +``` + +By enabling debug mode, the *localhost* file location is logged to the console so that it's possible to open it with a text editor when working on the simulator. When using the device to run the app, the file can be modified by overwriting the app's bundle from the **Device and Simulators** tool. + +## Listener + +Split SDKs send impression data back to Split servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression handler*. + +The SDK sends the generated impressions to the impression handler right away. As a result, be careful while implementing handling logic to avoid blocking the main thread. Generally speaking, you should create a separate thread to handle incoming impressions. Refer to the snippet below. + +```swift title="Swift" +let config = SplitClientConfig() +config.impressionListener = { impression in + // Do some work on main thread + DispatchQueue.global().async { + // Do some async work (use this most of the time!) + } +} + +let key: Key = Key(matchingKey: "key") +let builder = DefaultSplitFactoryBuilder() +let factory = +builder.setApiKey(apiKey).setKey(key).setConfig(config).build() +let client = factory?.client +``` + +In regards with the data available here, refer to the `impression` objects interface and description of each field below. There are two fields in particular that are different for Swift and Obj-C so see the corresponding tab: + +```swift title="Swift" + feature: String? + keyName: String? + treatment: String? + time: Int64? + changeNumber: Int64? + label: String? + bucketingKey: String? + attributes: [String: Any]? +``` + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| keyName | String? | Key which is evaluated. | +| bucketingKey | String? | Key which is used for bucketing, if provided. | +| feature | String? | Feature flag which is evaluated. | +| treatment | String? | Treatment that is returned. | +| time/timestamp | Int64?/NSNumber? | Timestamp of when the impression is generated. | +| label | String? | Targeting rule in the definition that matched resulting in the treatment being returned. | +| changeNumber/changeNum | Int64?/NSNumber? | Date and time of the last change to the targeting rule that the SDK used when it served the treatment. It is important to understand when a change made to a feature flag got picked up by the SDKs and whether one of the SDK instances is not picking up changes. | +| attributes | [String: Any]? | A map of attributes passed to `getTreatment`/`getTreatments`, if any. | + +## Flush + +The flush() method sends the data stored in memory (impressions and events) to Split cloud and clears the successfully posted data. If a connection issue is experienced, the data will be sent on the next attempt. + +```swift title="Swift" +client.flush() +``` + +## Background synchronization + +Since version 2.11.0, background synchronization is available for devices having iOS 13+. +To enable this feature, just follow the next 4 steps: + +1. Enable _[Background Mode Fetch](https://developer.apple.com/documentation/watchkit/background_execution/enabling_background_sessions)_ capability for your app. +2. Add the Split SDK background sync task identifier *io.split.bg-sync.task* to the Permitted background task scheduler identifiers section of the Info.plist . +3. Set the Split config flag _synchronizeInBackground_ to true . + +```swift title="Swift" +let config = SplitClientConfig() +config.synchronizeInBackground = true + +... +``` + +4. Schedule the background sync during app startup. e.g., _application(\_:didFinishLaunchingWithOptions:)_ + +```swift title="Swift" +SplitBgSynchronizer.shared.schedule() + +... +``` + +:::warning[Important!] +Due to an iOS limitation for background fetch capability, only one task identifier is allowed for that purpose. If there is already a current background fetch identifier registered, background sync may not work. +::: + +## Logging + +To enable SDK logging, the `logLevel` setting is available in `SplitClientConfig` class: + +```swift title="Setup logs" +// This setting type is `SplitLogLevel`. +// The available values are .verbose, .debug, .info, .warning, .error and .none + let config = SplitClientConfig() + config.logLevel = .verbose + + ... +``` + +The following shows an example output: + +

+ ios_log_example.png +

+ +## Advanced use cases + +This section describes advanced use cases and features provided by the SDK. + +### Instantiate multiple SDK clients + +In versions previous to 2.14.0, you had to create more that one SDK instance to evaluate for different users IDs. From v2.14.0 on, Split supports the ability to create multiple clients, one for each user ID. For example, if you need to roll out feature flags for different user IDs, you can instantiate multiple clients, one for each ID. You can then evaluate them using the corresponding client. You can do this using the example below: + +```swift title="Swift" +// Create factory +let key = Key(matchingKey: "anonymous_user") +let config = SplitClientConfig() +let factory = DefaultSplitFactoryBuilder().setApiKey(authorizationKey) + .setKey(key) + .setConfig(config).build() + +// Now when you call factory.client, the SDK will create a client +// using the anonymous_user key +// you passed in during the factory creation +let anonymousClient = factory.client + +// To create another client for a user instead, pass in a User ID +let userClient = factory.client(matchingKey: "user_id") + +// Add events handler for each client to be notified when SDK is ready +anonymousClient.on(event: SplitEvent.sdkReady, execute: { + // anonymousClient is ready to evaluate + // Check treatment for anonymous users + let accountPermissioningTreatment = anonymousClient.getTreatment("some_feature_flag") +}) + +userClient.on(event: SplitEvent.sdkReady, execute: { + // userClient is ready to evaluate + // Check treatment for the feature flag and user_id + let userPollTreatment = userClient.getTreatment("some_feature_flag") +}) +``` + +:::info[Number of SDK instances] +While the SDK does not put any limitations on the number of instances that you can create, we strongly recommend keeping the number of SDKs down to **one** or **two**. +::: + +### Subscribe to events + +You can listen for four different events from the SDK. + +* `sdkReadyFromCache`. This event fires once the SDK is ready to evaluate treatments using a locally cached version of your rollout plan from a previous session (which might be stale). If there is data in the cache, this event fires almost immediately, since access to the cache is fast; otherwise, it doesn't fire. +* ` sdkReady`. This event fires once the SDK is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Split servers. +* ` sdkReadyTimedOut`. This event fires if there is no cached version of your rollout plan in disk cache, and the SDK could not fully download the data from Split servers within the time specified by the `sdkReadyTimeOut` property of the `SplitClientConfig` object. This event does not indicate that the SDK initialization was interrupted. The SDK continues downloading the rollout plan and fires the `sdkReady` event when finished. This delayed `sdkReady` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. +* `sdkUpdated`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in the Split user interface. + +SDK event handling is done through the function `on(event:execute:)`, which receives a closure as an event handler. + +The code within the closure is executed on the main thread. For that reason, running code in the background must be done explicitly. + +The syntax to listen for an event is shown below. + +```swift title="Swift" +... +let client = factory.client + +client.on(event: SplitEvent.sdkReady, execute: { + // The client is ready to evaluate treatments according to the latest feature flag definitions + // Do some stuff on main thread +}) + +// Or +client.on(event: SplitEvent.sdkReadyTimedOut) { + // This callback will be called if and only if the client is configured with ready timeout and + // is not ready for that time or if the API key is wrong. + // You can still call getTreatment() but it could return CONTROL. + + // Do some stuff on main thread +} + +client.on(event: SplitEvent.sdkReadyFromCache) { + // Fired after the SDK could confirm the presence of the Split data. + // This event fires quickly, since there's no actual fetching of information. + // Keep in mind that data might be stale, this is NOT a replacement of sdkReady. + + // Do some stuff on main thread +} + +client.on(event: SplitEvent.sdkUpdated) { + // fired each time the client state change. + // For example, when a feature flag or segment changes. + + // Do some stuff on main thread +} + +... +``` + +### User consent + +The SDK allows you to disable the tracking of events and impressions until user consent is explicitly granted or declined. + +The `userConsent` configuration parameter lets you set the initial consent status of the SDK instance, and the factory method `setUserConsent(enabled: Bool)` lets you grant (enable) or decline (disable) dynamic data tracking. + +There are three possible initial states: + * `'GRANTED'`: The user grants consent for tracking events and impressions. The SDK sends them to Split cloud. This is the default value if `userConsent` param is not defined. + * `'DECLINED'`: The user declines consent for tracking events and impressions. The SDK does not send them to Split cloud. + * `'UNKNOWN'`: The user neither grants nor declines consent for tracking events and impressions. The SDK tracks them in its internal storage, and eventually either sends them or not if the consent status is updated to `'GRANTED'` or `'DECLINED'` respectively. + +The status can be updated at any time with the `setUserConsent` factory method. + +Working with user consent is demonstrated below. + +```swift title="User consent: Initial config, getter and setter" + let config = SplitClientConfig() + // Overwrites the initial consent status of the factory instance, which is 'GRANTED' by default. + // 'UNKNOWN' status represents that the user has neither granted nor declined consent for tracking data, + + // so the SDK locally tracks data but not send it to Split cloud until consent is changed to 'GRANTED'. + config.userConsent = .unknown + guard let factory = DefaultSplitFactoryBuilder() + .setApiKey("YOUR_SDK_KEY") + .setKey(Key(matchingKey: "user_key")) + .setConfig(config) + .build() else { + return + } + + // Changed User Consent status to 'GRANTED'. Data will be sent to Split cloud. + factory.setUserConsent(enabled: true); + // Changed User Consent status to 'DECLINED'. Data will not be sent to Split cloud. + factory.setUserConsent(enabled: false); + + // The 'getUserConsent' method returns User Consent status. + // We expose the constants for customer checks and tracking. + if (factory.userConsent == UserConsent.declined) { + print("USER CONSENT DECLINED"); + } + if (factory.userConsent == UserConsent.granted) { + print("USER CONSENT GRANTED"); + } + if (factory.userConsent == UserConsent.unknown) { + print("USER CONSENT UNKNOWN"); + } +``` + +### Certificate pinning + +The SDK allows you to constrain the certificates that the SDK trusts, using one of the following techniques: + +1. Pin a certificate's `SubjectPublicKeyInfo`, by providing the public key as a ___base64 SHA-256___ hash or a ___base64 SHA-1___ hash. +2. Pin a certificate's entire certificate chain (the root, all intermediate, and the leaf certificate), by providing the certificate chain as a .der file. + +Each pin corresponds to a host. For subdomains, you can optionally use wildcards, where `*` will match one subdomain (e.g. `*.example.com`), and `**` will match any number of subdomains (e.g `**.example.com`). + +You can optionally configure a handler to execute on certificate validation failure for a host. + +To set the SDK to require pinned certificates for specific hosts, add the `CertificatePinningConfig` object to `SplitClientConfig`, as shown below. + +```swift title="Swift" +// Define pins for certificate pinning +let certBuilder = CertificatePinningConfig.builder() + +// Provide a base 64 SHA-256 hash +certBuilder.addPin(host: "www.example1.com", hashKey: "sha256/7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=") + +// Provide a certificate file name. This file has to be added to the bundle. +certBuilder.addPin(host: "www.example2.com", certificateName: "certificate.der") + +// Set a failure handler +certBuilder.certificatePinningConfig { host in + print("Failed validation for host \(host)") +} + +// Set the CertificatePinningConfig property for the Split client configuration +let config = SplitClientConfig() +config.certificatePinningConfig = certBuilder.build() +// you can add other configuration properties here + +... +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/javascript-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/javascript-sdk.md new file mode 100644 index 00000000000..2035d8c572b --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/javascript-sdk.md @@ -0,0 +1,1240 @@ +--- +title: JavaScript SDK +sidebar_label: JavaScript SDK +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +This guide provides detailed information about our JavaScript SDK. All of our SDKs are open source. Go to our [JavaScript SDK GitHub repository](https://github.com/splitio/javascript-client) to see the source code. + +:::info[Migrating from v10.x to v11.x] +When upgrading, consider that the traffic type is no longer configured for the SDK client, and must be sent on the `client.track()` method call instead. + +Refer to the [migration guide](https://github.com/splitio/javascript-client/blob/development/MIGRATION-GUIDE.md) for complete information on upgrading to v11.x. +::: + +## Language support + +The JavaScript SDK supports all major browsers. While the library was built to support ES5 syntax, it depends on native support for ES6 `Promise`, `Map`, and `Set` objects, and therefore, you need to **polyfill** them if they are not available in your target browsers. + +If you're looking for possible polyfill options, check [es6-promise](https://github.com/stefanpenner/es6-promise), [es6-map](https://github.com/medikoo/es6-map) and [es6-set](https://github.com/medikoo/es6-set) for Promise, Map and Set polyfills respectively. + +## Initialization + +Set up Split in your code base with two simple steps. + +### 1. Import the SDK into your project + +You can import the SDK into your project using either of the three methods below. + + + +```bash +npm install --save @splitsoftware/splitio +``` + + +```html + +``` + + + +### 2. Instantiate the SDK and create a new Split client + + + +```javascript +// Instantiate the SDK. CDN will expose splitio globally +var factory = splitio({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + // key represents your internal user id, or the account id that + // the user belongs to. + // This could also be a cookie you generate for anonymous users + key: 'key' + } +}); + +// And get the client instance you'll use +var client = factory.client(); +``` + + +```javascript +var SplitFactory = require('@splitsoftware/splitio').SplitFactory; + +// Instantiate the SDK +var factory = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + // key represents your internal user id, or the account id that + // the user belongs to. + // This could also be a cookie you generate for anonymous users + key: 'key' + } +}); + +// And get the client instance you'll use +var client = factory.client(); +``` + + +```javascript +// Use the import let = require syntax on TS. +import { SplitFactory } from '@splitsoftware/splitio'; + +// Instantiate the SDK +const factory: SplitIO.IBrowserSDK = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + // the key can be the logged in + // user id, or the account id that + // the logged in user belongs to. + // The type of customer (user, account, custom) + // is chosen during Split's sign-up process. + key: 'key' + }, + startup: { + readyTimeout: 1.5 // 1.5 sec + } +}); + +// And get the client instance you'll use +const client: SplitIO.IBrowserClient = factory.client(); +``` + + + +:::warning[Updating to v10 for NPM version] +If you are using the CDN package or Bower, no changes are needed on your current code. +We changed our module system to ES modules and now we are exposing an object with a `SplitFactory` property. That property points to the same factory function that we were returning in the previous versions. Refer to the snippet above to see the code. +::: + +:::info[Notice for TypeScript] +With the SDK package on NPM, you get the SplitIO namespace, which contains useful types and interfaces for you to use. + +Feel free to dive into the declaration files if IntelliSense is not enough. +::: + +We recommend instantiating the Split factory once as a singleton and reusing it throughout your application. + +Configure the SDK with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split UI, on your Admin settings page, API keys section. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. + + +## Using the SDK + +### Basic use + +When the SDK is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds, depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while it's in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns the [control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). + +To make sure the SDK properly loads before asking it for a treatment, block until the SDK is ready, as shown below. We set the client to listen for the `SDK_READY` event triggered by the SDK before asking for an evaluation. + +After the `SDK_READY` event fires, you can use the `getTreatment` method to return the proper treatment based on the feature flag name and the key you passed when instantiating the SDK. Then use an if-else-if block as shown below and insert the code for the different treatments that you defined in the Split user interface. Remember the final else branch in your code to handle the client returning control. + + + +```javascript +client.on(client.Event.SDK_READY, function() { + var treatment = client.getTreatment('FEATURE_FLAG_NAME'); + + if (treatment == "on") { + // insert code here to show on treatment + } else if (treatment == "off") { + // insert code here to show off treatment + } else { + // insert your control treatment code here + } +}); +``` + + +```javascript +client.on(client.Event.SDK_READY, function() { + const treatment: SplitIO.Treatment = client.getTreatment('FEATURE_FLAG_NAME'); + + if (treatment == "on") { + // insert code here to show on treatment + } else if (treatment == "off") { + // insert code here to show off treatment + } else { + // insert your control treatment code here + } +}); +``` + + + +### Attribute syntax + +To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK's `getTreatment` method needs to be passed an attribute map at runtime. + +In the example below, we are rolling out a feature to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in the Split user interface to decide whether to show the `on` or `off` treatment to this account. + +The `getTreatment` method has a number of variations that are described below. Each of these additionally has a variation that takes an attributes argument, which can defines attributes of the following types: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: + +* **Strings:** Use type String. +* **Numbers:** Use type Number. +* **Dates:** Use type Date and express the value in `milliseconds since epoch`.
**Note:** Milliseconds since epoch is expressed in UTC. If your date or date-time combination is in a different timezone, first convert it to UTC, then transform it to milliseconds since epoch. +* **Booleans:** Use type Boolean. +* **Sets:** Use type Array. + + + +```javascript +var attributes = { + // date attributes are handled as `millis since epoch` + registered_date: new Date('YYYY-MM-DDTHH:mm:ss.sssZ').getTime(), + // this string will be compared against a list called `plan_type` + plan_type: 'growth', + // this number will be compared against a const value called `deal_size` + deal_size: 10000, + // this boolean will be compared against a const value called `paying_customer` + paying_customer: true, + // this array will be compared against a set called `permissions` + permissions: ["read", "write"] +}; + +var treatment = client.getTreatment('FEATURE_FLAG_NAME', attributes); + +if (treatment === 'on') { + // insert on code here +} else if (treatment === 'off') { + // insert off code here +} else { + // insert control code here +} +``` + + +```javascript +const attributes: SplitIO.Attributes = { + // date attributes are handled as `millis since epoch` + registered_date: new Date('YYYY-MM-DDTHH:mm:ss.sssZ').getTime(), + // this string will be compared against a list called `plan_type` + plan_type: 'growth', + // this number will be compared against a const value called `deal_size` + deal_size: 10000, + // this array will be compared against a set called `permissions` + permissions: [‘read’, ‘write’] +}; + +const treatment: SplitIO.Treatment = client.getTreatment('FEATURE_FLAG_NAME', attributes); + +if (treatment === 'on') { + // insert on code here +} else if (treatment === 'off') { + // insert off code here +} else { + // insert control code here +} +``` + + + +You can pass your attributes in exactly this way to the `client.getTreatments` method. + +### Binding attributes to the client + +Attributes can optionally be bound to the client at any time during the SDK lifecycle. These attributes are stored in memory and used in every evaluation to avoid the need to keep the attribute set accessible through the whole app. When an evaluation is called, the attributes provided (if any) at evaluation time are combined with the ones that are already loaded into the SDK memory, with the ones provided at function execution time taking precedence. This enables those attributes to be overridden or hidden for specific evaluations. + +An attribute is considered valid if it follows one of the types listed below: +- String +- Number +- Boolean +- Array + +The SDK validates these before storing them and if there are invalid or missing values, possibly indicating an issue, the methods return the boolean `false` and do not update any value. + +To use these methods, refer to the example below: + +```javascript title="JavaScript" +var attributes = { + registered_date: new Date('YYYY-MM-DDTHH:mm:ss.sssZ').getTime(), + plan_type: 'growth', + deal_size: 10000, + paying_customer: true, + permissions: ["read", "write"] +}; + +// set attributes returns true unless there is an issue storing it +var result = client.setAttributes(attributes); + +// set one attribute and returns true unless there is an issue storing it +var result = client.setAttribute('paying_customer', false); + +// Get an attribute +var plan_type = client.getAttribute('plan_type'); + +// Get all attributes +var stored_attributes = client.getAttributes(); + +// Remove an attribute +var result = client.removeAttribute('permissions'); + +// Remove all attributes +var result = client.clearAttributes(); + +``` + +### Multiple evaluations at once + +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` from the Split client to do this. +* `getTreatments`: Pass a list of the feature flag names you want treatments for. +* `getTreatmentsByFlagSet`: Evaluate all flags that are part of the provided set name and are cached on the SDK instance. +* `getTreatmentsByFlagSets`: Evaluate all flags that are part of the provided set names and are cached on the SDK instance. + + + +```javascript +// Getting treatments by feature flag names +var flagNames = ['FEATURE_FLAG_NAME_1', 'FEATURE_FLAG_NAME_2']; +var treatments = client.getTreatments(flagNames); + +// Getting treatments by set +treatments = client.getTreatmentsByFlagSet('frontend'); + +// Getting treatments for the union of multiple sets +var flagSets = ['frontend', 'client_side']; +treatments = client.getTreatmentsByFlagSets(flagSets); + +// treatments have the following form: +// { +// FEATURE_FLAG_NAME_1: 'on', +// FEATURE_FLAG_NAME_2: 'visa' +// } +``` + + +```javascript +// Getting treatments by feature flag names +const flagNames = ['FEATURE_FLAG_NAME_1', 'FEATURE_FLAG_NAME_2']; +let treatments: SplitIO.Treatments = client.getTreatments(flagNames); + +// Getting treatments by set +treatments = client.getTreatmentsByFlagSet('frontend'); + +// Getting treatments for the union of multiple sets +const flagSets = ['frontend', 'client_side']; +treatments = client.getTreatmentsByFlagSets(flagSets); + +// treatments have the following form: +// { +// FEATURE_FLAG_NAME_1: 'on', +// FEATURE_FLAG_NAME_2: 'visa' +// } +``` + + + +### Get Treatments with Configurations + +To [leverage dynamic configurations with your treatments](https://help.split.io/hc/en-us/articles/360026943552), use the `getTreatmentWithConfig` method. + +This method returns an object with the structure below: + + + +```javascript +var TreatmentResult = { + String treatment; + String config; // or null if there is no config for the treatment +} +``` + + +```javascript +type TreatmentResult = { + treatment: string, + config: string | null +}; +``` + + + +As you can see from the object structure, the config is a stringified version of the configuration JSON defined in the Split user interface. If there is no configuration defined for a treatment, the SDK returns `null` for the config parameter. + +This method takes the exact same set of arguments as the standard `getTreatment` method. Refer to the examples below for proper usage: + + + +```javascript +var treatmentResult = client.getTreatmentWithConfig('FEATURE_FLAG_NAME', attributes); +var configs = JSON.parse(treatmentResult.config); +var treatment = treatmentResult.treatment; + +if (treatment === 'on') { + // insert on code here and use configs here as necessary +} else if (treatment === 'off') { + // insert off code here and use configs here as necessary +} else { + // insert control code here +} +``` + + +```javascript +const treatmentResult: SplitIO.TreatmentWithConfig = client.getTreatmentWithConfig('FEATURE_FLAG_NAME', attributes); +const configs = JSON.parse(treatmentResult.config); +const treatment = treatmentResult.treatment; + +if (treatment === 'on') { + // insert on code here and use configs here as necessary +} else if (treatment === 'off') { + // insert off code here and use configs here as necessary +} else { + // insert control code here +} +``` + + + +If you need to get multiple evaluations at once, you can also use the `getTreatmentsWithConfig` methods. These methods take the exact same arguments as the [getTreatments](#multiple-evaluations-at-once) methods but return a mapping of feature flag names to TreatmentResults instead of strings. Example usage below: + + + +```javascript +// Getting treatments by feature flag names +var featureFlagNames = ['FEATURE_FLAG_NAME_1', 'FEATURE_FLAG_NAME_2']; + +var treatmentResults = client.getTreatmentsWithConfig(featureFlagNames); + +// Getting treatments by set +treatmentResults = client.getTreatmentsWithConfigByFlagSet('frontend'); + +// Getting treatments for the union of multiple sets +var flagSets = ['frontend', 'client_side']; +treatmentResults = client.getTreatmentsWithConfigByFlagSets(flagSets); + +// treatmentResults will have the following form: +// { +// FEATURE_FLAG_NAME_1: {treatment: 'on', +// config: "{ 'color' : 'red'}}", +// FEATURE_FLAG_NAME_2: {treatment: 'v2', +// config: "{ 'copy' : 'better copy'}}", +// } +``` + + +```javascript +// Getting treatments by feature flag names +const featureFlagNames = ['FEATURE_FLAG_NAME_1', 'FEATURE_FLAG_NAME_2']; + +let treatmentResults: SplitIO.TreatmentsWithConfig = client.getTreatmentsWithConfig(featureFlagNames); + +// Getting treatments by set +treatmentResults = client.getTreatmentsWithConfigByFlagSet('frontend'); + +// Getting treatments for the union of multiple sets +const flagSets = ['frontend', 'client_side']; +treatmentResults = client.getTreatmentsWithConfigByFlagSets(flagSets); + +// treatmentResults will have the following form: +// { +// FEATURE_FLAG_NAME_1: {treatment: 'on', +// config: "{ 'color' : 'red'}}", +// FEATURE_FLAG_NAME_2: {treatment: 'v2', +// config: "{ 'copy' : 'better copy'}}", +// } +``` + + + +### Shutdown + +Call the `client.destroy()` method before letting a process using the SDK exit, as this method gracefully shuts down the Split SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. + +```javascript title="JavaScript" +// You can just destroy and remove the variable reference and move on: +user_client.destroy(); +user_client = null; + +// destroy() returns a promise, so if you want to, for example, +// navigate to another page without losing impressions, you +// can do that once the promise resolves. +user_client.destroy().then(function() { + user_client = null; + + document.location.replace('another_page'); +}); +``` + +After `destroy()` is called and finishes, any subsequent invocations to `getTreatment`/`getTreatments` or manager methods result in `control` or empty list, respectively. + +:::warning[Important!] +A call to the `destroy()` method also destroys the factory object. When creating new client instance, first create a new factory instance. +::: + +## Track + +Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your features on your users’ actions and metrics. + +Learn more about [tracking events](https://help.split.io/hc/en-us/articles/360020585772) in Split. + +In the examples below you can see that the `.track()` method can take up to four arguments. The proper data type and syntax for each are: + +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: + * Contains 63 characters or fewer. + * Starts with a letter or number. + * Contains only letters, numbers, hyphen, underscore, or period. + * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` +* **VALUE:** (Optional) The value to be used in creating the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is **Integer** or **Float**. +* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. + +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event to be sent back to Split's servers on the next event post. The SDK will return `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method has been provided. + +In the case that a bad input has been provided, you can read more about our SDK's expected behavior [here](https://help.split.io/hc/en-us/articles/360020585772-Track-events) + + + +```javascript +// If you have only passed the key to the SDK +var queued = client.track('TRAFFIC_TYPE', 'EVENT_TYPE', eventValue, { properties }); +// Example with both a value and properties +var properties = {package : "premium", admin : true, discount : 50}; +var queued = client.track('user', 'page_load_time', 83.334, properties); + +// Example with only properties +var properties = {package : "premium", admin : true, discount : 50}; +var queued = client.track('user', 'page_load_time', null, properties); + +// Example with both a value and properties +var properties = {package : "premium", admin : true, discount : 50}; +var queued = client.track('user', 'page_load_time', 83.334, properties); + +// Example with only properties +var properties = {package : "premium", admin : true, discount : 50}; +var queued = client.track('user', 'page_load_time', null, properties); +``` + + +```javascript +// If you have only passed the key to the SDK +const queued: boolean = client.track('TRAFFIC_TYPE', 'EVENT_TYPE', eventValue, , { properties }); +// Example with both a value and properties +const properties = {package : "premium", admin : true, discount : 50}; +const queued = client.track('user', 'page_load_time', 83.334, properties); + +// Example with only properties +const properties = {package : "premium", admin : true, discount : 50}; +const queued = client.track('user', 'page_load_time', null, properties); + +// Example with both a value and properties +const properties = {package : "premium", admin : true, discount : 50}; +const queued = client.track('user', 'page_load_time', 83.334, properties); + +// Example with only properties +const properties = {package : "premium", admin : true, discount : 50}; +const queued = client.track('user', 'page_load_time', null, properties); +``` + + + +## Configuration + +The SDK has a number of knobs for configuring performance. Each knob is tuned to a reasonable default. However, you can override the value while instantiating the SDK. The parameters available for configuration are shown below. + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| core.labelsEnabled | Enable impression labels from being sent to Split backend. Labels may contain sensitive information. | true | +| startup.readyTimeout | Maximum amount of time in seconds to wait before firing the `SDK_READY_TIMED_OUT` event | 10 | +| startup.requestTimeoutBeforeReady | The SDK has two main endpoints it uses /splitChanges and /mySegments that it hits to get ready. This config sets how long (in seconds) the SDK waits for each request it makes as part of getting ready. | 5 | +| startup.retriesOnFailureBeforeReady | How many retries on /splitChanges and /mySegments we do while getting the SDK ready | 1 | +| startup.eventsFirstPushWindow | Use to set a specific timer (expressed in seconds) for the first push of events, starting on SDK initialization. | 10 | +| scheduler.featuresRefreshRate | The SDK polls Split servers for changes to feature rollout plans. This parameter controls this polling period in seconds. | 60 | +| scheduler.segmentsRefreshRate | The SDK polls Split servers for changes to segment definitions. This parameter controls this polling period in seconds. | 60 | +| scheduler.impressionsRefreshRate | The SDK sends information on who got what treatment at what time back to Split servers to power analytics. This parameter controls how often this data is sent to Split servers. The parameter should be in seconds. | 300 | +| scheduler.impressionsQueueSize | The max amount of impressions we queue. If the queue is full, the SDK flushes the impressions and resets the timer. | 30000 | +| scheduler.eventsPushRate | The SDK sends tracked events to Split servers. This setting controls that flushing rate in seconds. | 60 | +| scheduler.eventsQueueSize | The max amount of events we queue. If the queue is full, the SDK flushes the events and resets the timer. | 500 | +| scheduler.telemetryRefreshRate | The SDK caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers (in seconds). | 3600 seconds (1 hour) | +| sync.splitFilters | Filter specific feature flags to be synced and evaluated by the SDK. This is formed by a type string property and a list of string values for the given criteria. Using the types 'bySet' (recommended, flag sets are available in all tiers) or 'byName', pass an array of strings defining the query. If empty or unset, all feature flags are downloaded by the SDK. | [] | +| sync.impressionsMode | This configuration defines how impressions (decisioning events) are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Split; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, ALL impressions are queued and sent to Split; this is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. | `OPTIMIZED` | +| sync.enabled | Controls the SDK continuous synchronization flags. When `true`, a running SDK processes rollout plan updates performed in the Split user interface (default). When `false`, it fetches all data from the Split cloud only upon init, which ensures a consistent experience during a user session and optimizes resources when these updates are not consumed by the app. | true | +| sync.requestOptions.getHeaderOverrides | A callback function that can be used to override the Authentication header or append new headers to the SDK's HTTP(S) requests. | undefined | +| storage.type | Storage type to be used by the SDK. Possible values are `MEMORY` and `LOCALSTORAGE`. | `MEMORY` | +| storage.prefix | An optional prefix for your data to avoid collisions. This prefix is prepended to the existing SPLITIO localStorage prefix. | `SPLITIO` | +| debug | Either a boolean flag or log level string ('ERROR', 'WARN', 'INFO', or 'DEBUG'). See [logging](#logging) for details. | false | +| streamingEnabled | Boolean flag to enable the streaming service as default synchronization mechanism. In the event of an issue with streaming, the SDK falls back to the polling mechanism. If false, the SDK polls for changes as usual without attempting to use streaming. | true | +| userConsent | User consent status used to control the tracking of events and impressions. Possible values are `GRANTED`, `DECLINED`, and `UNKNOWN`. See [User consent](#user-consent) for details. | `GRANTED` | + +To set each of the parameters defined above, use the following syntax: + + + +```javascript +var sdk = SplitFactory({ + startup: { + requestTimeoutBeforeReady: 5, // 5 sec + retriesOnFailureBeforeReady: 1, // 1 sec + readyTimeout: 10, // 10 sec + eventsFirstPushWindow: 10 // 10 sec + }, + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'USER_ID' + }, + scheduler: { + featuresRefreshRate: 5, // 5 sec + segmentsRefreshRate: 60, // 60 sec + impressionsRefreshRate: 300, // 300 sec + impressionsQueueSize: 30000, // 30000 items + eventsPushRate: 60, // 60 sec + eventsQueueSize: 500, // 500 items + telemetryRefreshRate: 3600 // 1 hour + }, + sync: { + splitFilters: [{ + type: 'bySet', + values: ['frontend'] + }], + impressionsMode: 'NONE' + }, + storage: { + type: 'LOCALSTORAGE', + prefix: 'MYPREFIX' + }, + streamingEnabled: true, + debug: false +}); +``` + + +```javascript +const sdk: SplitIO.IBrowserSDK = SplitFactory({ + startup: { + requestTimeoutBeforeReady: 5, // 5 sec + retriesOnFailureBeforeReady: 1, // 1 sec + readyTimeout: 10, // 10 sec + eventsFirstPushWindow: 10 // 10 sec + }, + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'USER_ID' + }, + scheduler: { + featuresRefreshRate: 5, // 5 sec + segmentsRefreshRate: 60, // 60 sec + impressionsRefreshRate: 300, // 300 sec + impressionsQueueSize: 30000, // 30000 items + eventsPushRate: 60, // 60 sec + eventsQueueSize: 500, // 500 items + telemetryRefreshRate: 3600 // 1 hour + }, + sync: { + splitFilters: [{ + type: 'bySet', + values: ['frontend'] + }], + impressionsMode: 'NONE' + }, + storage: { + type: 'LOCALSTORAGE', + prefix: 'MYPREFIX' + }, + streamingEnabled: true, + debug: false +}); +``` + + + +## Localhost mode + +For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the Split SDK can be started in **localhost** mode (aka off-the-grid or offline mode). In this mode, the SDK neither polls nor updates Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the features. + +When instantiating the SDK in localhost mode, your `authorizationKey` is `localhost`. Define the feature flags you want to use in the `features` object map. All `getTreatment` calls for a feature flag now only return the one treatment (and config, if defined) that you have defined in the map. + +Any feature that is not provided in the `features` map returns the [control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment) if the SDK was asked to evaluate them. + +You can use the additional configuration parameters below when instantiating the SDK in `localhost` mode. + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| scheduler.offlineRefreshRate | The refresh interval for the mocked features treatments. | 15 | +| features | A fixed mapping of which treatment to show for our mocked features. | {}
By default we have no mocked features. | + +To use the SDK in localhost mode, replace the SDK key on `authorizationKey` property with `'localhost'`, as shown in the example below. Note that you can define in the `features` object a feature flag name and its treatment directly or use a map to define both a treatment and a dynamic configuration. + +If you define just a string as the value for a feature flag name, any config returned by our SDKs are always null. If you use a map, we return the specified treatment and the specified config (which can also be null). + + + + +```javascript +var sdk = splitio({ + core: { + authorizationKey: 'localhost', + key: 'CUSTOMER_ID' + }, + features: { + 'reporting_v2': 'on', // example with just a string value for the treatment + 'billing_updates': { treatment: 'visa', config: '{ "color": "blue" }' }, // example of a defined config + 'show_status_bar': { treatment: 'off', config: null } // example of a null config + }, + scheduler: { + offlineRefreshRate: 15 // 15 sec + } +}); + +var client = sdk.client(); + +// The following code will be evaluated once the engine finishes the initialization +client.on(client.Event.SDK_READY, function() { + // The sentence below will return 'on' + var t1 = client.getTreatment('reporting_v2') + // The sentence below will return an object with the structure of: {treatment:'visa',config:'{ "color":"blue" }'} + var t2 = client.getTreatmentWithConfig('billing_updates') + // The sentence below will return 'control' because that feature does not exist + var t3 = client.getTreatmentWithConfig('navigation_bar_changes') +}); + +// The following code will be evaluated only if using the LocalStorage option as storage type. +// The only difference with the production mode is that the event is always emitted in localhost mode, +// since the SDK can evaluate using the features provided by your `features` config object. +client.on(client.Event.SDK_READY_FROM_CACHE, function() { + // The sentence below will return 'on' + var t1 = client.getTreatment('reporting_v2') +} +``` + + +```javascript +const sdk: SplitIO.IBrowserSDK = SplitFactory({ + core: { + authorizationKey: 'localhost', + key: 'CUSTOMER_ID' + }, + features: { + 'reporting_v2': 'on', // example with just a string value for the treatment + 'billing_updates': { treatment: 'visa', config: '{ "color": "blue"}' } // example of a defined config + 'show_status_bar': { treatment: 'off', config: null } // example of a null config + }, + scheduler: { + offlineRefreshRate: 15 // 15 sec + } +}); + +const client: SplitIO.IBrowserClient = sdk.client(); + +// The following code will be evaluated once the engine finishes the initialization +client.on(client.Event.SDK_READY, () => { + // The sentence below will return 'on' + const t1: SplitIO.Treatment = client.getTreatment('reporting_v2'); + // The sentence below will return an object with the structure of: {treatment:'visa',config:'{ "color":"blue" }' + const t2: SplitIO.Treatment = client.getTreatmentWithConfig('billing_updates'); + // The sentence below will return 'control' because that feature does not exist + const t3: SplitIO.Treatment = client.getTreatmentWithConfig('navigation_bar_changes'); +}); + +// The following code will be evaluated only if using the LocalStorage option as storage type. +// The only difference with the production mode is that the event is always emitted in localhost mode, +// since the SDK can evaluate using the features provided by your `features` config object. +client.on(client.Event.SDK_READY_FROM_CACHE, () => { + // The sentence below will return 'on' + const t1: SplitIO.Treatment = client.getTreatment('reporting_v2') +} +``` + + + +You can then change the feature flags as necessary for your testing, by mutating the properties of the `features` object you've provided. The SDK simulates polling for changes every `offlineRefreshRate` seconds, and will emit an `SDK_UPDATE` event if the mocked features have changed. + + + +```javascript +// The SDK keeps a reference to the `features` object map, so you can mutate the object as follows to emit SDK_UPDATE events: +config.features['reporting_v2'] = 'off'; // update reporting_v2 +config.features['reporting_v3'] = 'off'; // add reporting_v3 +delete config.features['reporting_v2']; // delete reporting_v2 + +// In case you need to update the whole mock object, you can replace the internal reference from the factory: +factory.settings.features = { 'reporting_v3': 'off' }; + +// But don't do it on the passed configuration, as the SDK will not reference the new object: +config.features = { 'reporting_v3': 'off' }; // Will not emit SDK_UPDATE +``` + + + +## Manager + +Use the Split manager to get a list of features available to the Split client. To instantiate a Manager in your code base, use the same factory that you used for your client. + + + +```javascript +var factory = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + // the key can be the logged in + // user id, or the account id that + // the logged in user belongs to. + // The type of customer (user, account, custom) + // is chosen during Split's sign-up process. + key: 'key' + } +}); + +var manager = factory.manager(); + +manager.once(manager.Event.SDK_READY, function() { + // Once it's ready, use the manager +}); +``` + + +```javascript +const factory: SplitIO.IBrowserSDK = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + // the key can be the logged in + // user id, or the account id that + // the logged in user belongs to. + // The type of customer (user, account, custom) + // is chosen during Split's sign-up process. + key: 'key' + } +}); + +const manager: SplitIO.IManager = factory.manager(); + +manager.once(manager.Event.SDK_READY, function() { + // Once it's ready, use the manager +}); +``` + + + +The Manager then has the following methods available: + + + +```javascript +/** + * Returns the feature flag registered within the SDK that matches this name. + * + * @return SplitView or null. + */ +var splitView = manager.split('name-of-feature-flag'); + +/** + * Retrieves all the feature flags that are currently registered within the SDK. + * + * returns a List of SplitViews. + */ +var splitViewsList = manager.splits(); + +/** + * Returns the names of all features flags registered within the SDK. + * + * @return a List of Strings of the features' names. + */ +var splitNamesList = manager.names(); +``` + + +```javascript +/** + * Returns the feature flag registered within the SDK that matches this name. + * + * @return SplitView or null. + */ +const splitView: SplitIO.SplitView = manager.split('name-of-feature-flag'); + +/** + * Retrieves all the feature flags that are currently registered within the SDK. + * + * returns a List of SplitViews. + */ +const splitViewsList: SplitIO.SplitViews = manager.splits(); + +/** + * Returns the names of all features flags registered within the SDK. + * + * @return a List of Strings of the features' names. + */ +const splitNamesList: SplitIO.SplitNames = manager.names(); +``` + + + +The `SplitView` object referenced above has the following structure: + +```typescript title="TypeScript" +type SplitView = { + name: string, + trafficType: string, + killed: boolean, + treatments: Array, + changeNumber: number, + configs: { + [treatmentName: string]: string + }, + defaultTreatment: string, + sets: Array, + impressionsDisabled: boolean +} +``` + +## Listener + +Split SDKs send impression data back to Split servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression listener*. For that purpose, the SDK's configurations have a parameter called `impressionListener` where an implementation of `ImpressionListener` could be added. This implementation **must** define the `logImpression` method and it receives data in the following schema. + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| impression | Object | Impression object that has the feature, key, treatment, label, etc. | +| attributes | Object | A map of attributes passed to `getTreatment`/`getTreatments` (if any). | +| sdkLanguageVersion | String| The version of the SDK. In this case the language is `javascript` plus the version currently running. | + +:::info[Note] +There are two additional keys on this object, `ip` and `hostname`. They are not used on the browser. +::: + +## Implement custom impression listener + +The following is an example of how to implement a custom impression listener: + + + +```javascript +function logImpression(impressionData) { + // do something with the impression data. +} + +var factory = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + impressionListener: { + logImpression: logImpression + } +}); +``` + + +```javascript +class MyImprListener implements SplitIO.IImpressionListener { + logImpression(impressionData: SplitIO.ImpressionData) { + // do something with impressionData + } +} + +const factory: SplitIO.IBrowserSDK = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + impressionListener: { + logImpression: new MyImprListener() + } +}); +``` + + + +An impression listener is called asynchronously from the corresponding evaluation, but is almost immediate. + +Even though the SDK does not fail, if there is an exception in the listener, do not block the call stack. + +## Logging + +To enable SDK logging in the browser, open your DevTools console and type the following: + +```javascript title="Enable logging from browser console" +// Acceptable values are 'DEBUG', 'INFO', 'WARN', 'ERROR' and 'NONE' +// Other acceptable values are 'on', 'enable' and 'enabled', which are equivalent to 'DEBUG' log level +localStorage.splitio_debug = 'on' +``` + +Reload the browser to start seeing the logs. + +Beginning with v9.2.0 of the SDK, you can also enable the logging via SDK settings and programmatically by calling the Logger API. + + + +```javascript +var SplitFactory = require('@splitsoftware/splitio').SplitFactory; + +var sdk = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + debug: true // Debug boolean option can be passed on settings + // It takes precedence over the localStorage flag. +}); + +// Or you can use the Logger API which two methods, enable and disable. +// Calling this methods will have an immediate effect. +sdk.Logger.enable(); +sdk.Logger.disable(); + +// You can also set the log level programatically after v10.4.0 +// Acceptable values are: 'DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE'. +// 'DEBUG' is equivalent to `enable` method. +// 'NONE' is equivalent to `disable` method. +sdk.Logger.setLogLevel('WARN'); +``` + + +```javascript +import { SplitFactory } from '@splitsoftware/splitio'; + +const sdk: SplitIO.IBrowserSDK = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + debug: true // Debug boolean option can be passed on settings. + // It takes precedence over the localStorage flag. +}); + +// Or you can use the Logger API which two methods, enable and disable. +// Calling this methods will have an immediate effect. +sdk.Logger.enable(); +sdk.Logger.disable(); + +// You can also set the log level programatically after v10.4.0 +// Acceptable values are: 'DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE'. +// 'DEBUG' is equivalent to `enable` method. +// 'NONE' is equivalent to `disable` method. +sdk.Logger.setLogLevel('WARN'); +``` + + + +Example output is shown below. + +

7694b46-Captura_de_pantalla_2017-05-05_a_las_13.04.08.png

+ +:::info[Note] +For more information on using the logging framework in SDK versions prior to 9.2, refer to [https://github.com/visionmedia/debug](https://github.com/visionmedia/debug). +::: + +## Advanced use cases + +This section describes advanced use cases and features provided by the SDK. + +### Instantiate multiple SDK clients + +Each JavaScript SDK client is tied to one specific customer and traffic type at a time (for example, `user`, `account`, `organization`). This enhances performance and reduces data cached within the SDK. + +Split supports the ability to release based on multiple traffic types. With traffic types, you can release to `users` in one feature flag and `accounts` in another. If you are unfamiliar with using multiple traffic types, refer to [Traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) for more information. + +If you need to roll out features by different traffic types, instantiate multiple SDK clients, one for each traffic type. For example, you may want to roll out the feature `user-poll` by `users` and the feature `account-permissioning` by `accounts`. You can do this with the example below: + + + +```javascript +var factory = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'CUSTOMER_ACCOUNT_ID', + // Instantiate the sdk once and provide the ID for one of the + // traffic types that you plan to release to. It doesn't + // matter which you pick to start off with. + }, +}); + +// now when you call factory.client(), the sdk will create a client +// using the Account ID you passed in during the factory creation. +var account_client = factory.client(); + +// to create another client for a User traffic type instead, +// just pass in a User ID to the factory.client() method. +// This is only valid after at least one client has been initialized. +var user_client = factory.client('CUSTOMER_USER_ID'); + +// check treatment for user-poll and CUSTOMER_USER_ID +var user_poll_treatment = user_client.getTreatment('user-poll'); + +// check treatment for account-permissioning and CUSTOMER_ACCOUNT_ID +var account_permissioning_treatment = account_client.getTreatment('account-permissioning'); + +// track events for user traffic type +user_client.track('user', 'PAGELOAD', 7.86); + +// or track events for account traffic type +account_client.track('account', 'ACCOUNT_CREATED'); +``` + + +```javascript +const sdk: SplitIO.IBrowserSDK = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'CUSTOMER_ACCOUNT_ID' + // instantiate the sdk once and provide the ID for one of the + // traffic types that you plan to release to. It doesn't + // matter which you pick to start off with. + }, +}); + +// now when you call sdk.client(), the sdk will create a client +// using the Account ID you passed in during the factory creation. +const account_client: SplitIO.IBrowserClient = factory.client(); + +// to create another client for a User instead, just pass in a +// User ID to the sdk.client() method. This is only valid after +// at least one client has been initialized. +const user_client: SplitIO.IBrowserClient = + factory.client('CUSTOMER_USER_ID'); + +// check treatment for user-poll and CUSTOMER_USER_ID +const user_poll_treatment: SplitIO.Treatment = + user_client.getTreatment('user-poll'); + +// check treatment for account-permissioning and CUSTOMER_ACCOUNT_ID +const account_permissioning_treatment: SplitIO.Treatment = + account_client.getTreatment('account-permissioning'); +``` + + + +:::info[Number of SDK instances] +While the SDK does not put any limitations on the number of instances that can be created, we strongly recommend keeping the number of SDKs down to **one** or **two**. +::: + +### Subscribe to events + +You can listen for four different events from the SDK. + +* `SDK_READY_FROM_CACHE`. This event fires once the SDK is ready to evaluate treatments using a version of your rollout plan cached in localStorage from a previous session (which might be stale). If there is data in localStorage, this event fires almost immediately, since access to localStorage is fast; otherwise, it doesn't fire. +* `SDK_READY`. This event fires once the SDK is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Split servers. +* `SDK_READY_TIMED_OUT`. This event fires if there is no cached version of your rollout plan cached in localStorage, and the SDK could not download the data from Split servers within the time specified by the `readyTimeout` configuration parameter. This event does not indicate that the SDK initialization was interrupted. The SDK continues downloading the rollout plan and fires the `SDK_READY` event when finished. This delayed `SDK_READY` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. +* `SDK_UPDATE`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in the Split user interface. + +The syntax to listen for each event is shown below: + + + +```javascript +function whenReady() { + var treatment = client.getTreatment('FEATURE_FLAG_NAME'); + + if (treatment === 'on') { + // insert on code + } else if (treatment === 'off') { + // insert off code + } else { + // insert control code (usually the same as default treatment) + } +} + +// the client is ready to evaluate treatments according to the latest flag definitions +client.once(client.Event.SDK_READY, whenReady); + +client.once(client.Event.SDK_READY_TIMED_OUT, function () { + // this callback will be called after the amount of time defined by startup.readyTimeout if and only if the client + // is not ready in that time. You can still call getTreatment() + // but it could return CONTROL. +}); + +client.on(client.Event.SDK_UPDATE, function () { + // fired each time the client state changes. + // For example, when a feature flag or segment changes. + console.log('The SDK has been updated!'); +}); + +// This event only fires using the LocalStorage option and if there's Split data stored in the browser. +client.once(client.Event.SDK_READY_FROM_CACHE, function () { + // Fired after the SDK could confirm the presence of the Split data. + // This event fires really quickly, since there's no actual fetching of information. + // Keep in mind that data might be stale, this is NOT a replacement of SDK_READY. +}); +``` + + +```javascript +function whenReady() { + const treatment: SplitIO.Treatment = client.getTreatment('FEATURE_FLAG_NAME'); + + if (treatment === 'on') { + // insert on code + } else if (treatment === 'off') { + // insert off code + } else { + // insert control code (usually the same as default treatment) + } +} + +// the client is ready for start making evaluations with your data +client.once(client.Event.SDK_READY, whenReady); + +client.once(client.Event.SDK_READY_TIMED_OUT, () => { + // this callback is called after 1.5 seconds if and only if the client + // is not ready for that time. You can still call getTreatment() + // but it could return CONTROL. +}); + +client.on(client.Event.SDK_UPDATE, () => { + // fired each time the client state change. + // For example, when a feature flag or segment changes. + console.log('The SDK has been updated!'); +}); + +// This event only fires using the LocalStorage option and if there's Split data stored in the browser. +client.once(client.Event.SDK_READY_FROM_CACHE, function () { + // Fired after the SDK could confirm the presence of the Split data. + // This event fires really quickly, since there's no actual fetching of information. + // Keep in mind that data might be stale, this is NOT a replacement of SDK_READY. +}); +``` + + + +### User consent + +The SDK allows you to disable the tracking of events and impressions until user consent is explicitly granted or declined. + +The `userConsent` configuration parameter lets you set the initial consent status of the SDK instance, and the factory method `UserConsent.setStatus(boolean)` lets you grant (enable) or decline (disable) dynamic data tracking. + +There are three possible initial states: + * `'GRANTED'`: The user grants consent for tracking events and impressions. The SDK sends them to Split cloud. This is the default value if `userConsent` param is not defined. + * `'DECLINED'`: The user declines consent for tracking events and impressions. The SDK does not send them to Split cloud. + * `'UNKNOWN'`: The user neither grants nor declines consent for tracking events and impressions. The SDK tracks them in its internal storage, and eventually either sends them or not if the consent status is updated to `'GRANTED'` or `'DECLINED'` respectively. + +The status can be updated at any time with the `UserConsent.setStatus` factory method. + +Working with user consent is demonstrated below. + +```javascript title="User consent: Initial config, getter and setter" +var factory = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + // Overwrites the initial consent status of the factory instance, which is 'GRANTED' by default. + // 'UNKNOWN' status represents that the user has neither granted nor declined consent for tracking data, + // so the SDK will locally track data but not send it to Split cloud until consent is changed to 'GRANTED'. + userConsent: 'UNKNOWN' +}); + +// `getStatus` method returns the current consent status. +factory.UserConsent.getStatus() === factory.UserConsent.Status.UNKNOWN; + +// `setStatus` method lets you update the factory consent status at any time. +// Pass `true` for 'GRANTED' and `false` for 'DECLINED'. +factory.UserConsent.setStatus(true); // Consent status changed from 'UNKNOWN' to 'GRANTED'. Data will be sent to Split cloud. +factory.UserConsent.getStatus() === factory.UserConsent.Status.GRANTED; + +factory.UserConsent.setStatus(false); // Consent status changed from 'GRANTED' to 'DECLINED'. Data will not be sent to Split cloud. +factory.UserConsent.getStatus() === factory.UserConsent.Status.DECLINED; +``` + +## Example apps + +The following example applications detail how to configure and instantiate the Split JavaScript SDK on commonly used platforms: + +* [Basic HTML](https://github.com/splitio/example-javascript-client) +* [AngularJS](https://github.com/splitio/angularjs-sdk-examples) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/react-native-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/react-native-sdk.md new file mode 100644 index 00000000000..c7f3c5a3d6e --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/react-native-sdk.md @@ -0,0 +1,1249 @@ +--- +title: React Native SDK +sidebar_label: React Native SDK +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +This guide provides detailed information about our React Native SDK. This SDK is built on top of our JS SDK core modules but is optimized for React Native applications. This SDK also has a pluggable API you can use to include more functionality optionally and keep your bundle leaner. + +If already using our isomorphic JavaScript SDK, consider this [migration guide](https://help.split.io/hc/en-us/articles/360059966112-Browser-SDK-Migration-Guide) to understand the changes of the new pluggable API. + +All of our SDKs are open source. Go to our [React Native SDK GitHub repository](https://github.com/splitio/react-native-client) to see the source code. + +:::info[Migrating from v0.x to v1.x] +Refer to this [migration guide](https://github.com/splitio/react-native-client/blob/development/MIGRATION-GUIDE.md) for complete information on updating to v1.x. +::: + +## Language support + +The Split SDK for React Native supports both React Native bare projects (using [React-Native CLI](https://reactnative.dev/docs/environment-setup)) and Expo managed projects (using [Expo CLI](https://docs.expo.io/get-started/installation/)). + +It has been validated with React Native v0.59 and later, and Expo v36 and later, but should also work with older versions. + +## Initialization + +Set up Split in your code base with two steps. + +### 1. Import the SDK into your project + +Install the package in your project: + + + +```bash +npm install @splitsoftware/splitio-react-native +``` + + +```bash +yarn add @splitsoftware/splitio-react-native +``` + + +```bash +expo install @splitsoftware/splitio-react-native +``` + + + +The Split SDKs support two synchronization mechanisms, **streaming** (default and recommended) and **polling** which is the fallback in cases where streaming is not supported or as a temporary measure in case of any issues detected on the persistent connection. We recommend following the steps below to enable the necessary support for the Event Source modules. + +- For React Native bare projects, you need to *link* the native modules of the package. + +If using React Native 0.59 or below, run `react-native link @splitsoftware/splitio-react-native` + +If using React Native 0.60+, the [autolink feature](https://github.com/react-native-community/cli/blob/master/docs/autolinking.md) is available and you don't need to run `react-native link`, but you still need to install the pods if developing for iOS, with the command `npx pod-install ios`. + +- For Expo managed projects, Split SDK native modules cannot be used, but you can still support streaming by *polyfilling* the global EventSource constructor: + +Install an EventSource implementation such as [react-native-event-source](https://www.npmjs.com/package/react-native-event-source): + +```bash +expo install react-native-event-source +``` + +Polyfill the global EventSource constructor, for example, by including the following in your project entrypoint file (e.g., `App.jsx`): + +```javascript +import RNEventSource from 'react-native-event-source'; + +globalThis.EventSource = RNEventSource; +``` + +### 2. Instantiate the SDK and create a new Split client + + + +```typescript +import { SplitFactory } from '@splitsoftware/splitio-react-native'; + +// Instantiate the SDK +const factory: SplitIO.IBrowserSDK = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + // key represents your internal user id, or the account id that + // the user belongs to. + // This could also be a cookie you generate for anonymous users + key: 'key' + } +}); + +// And get the client instance you'll use +const client: SplitIO.IBrowserClient = factory.client(); +``` + + +```javascript +var SplitFactory = require('@splitsoftware/splitio-react-native').SplitFactory; + +// Instantiate the SDK +var factory = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + // key represents your internal user id, or the account id that + // the user belongs to. + // This could also be a cookie you generate for anonymous users + key: 'key' + } +}); + +// And get the client instance you'll use +var client = factory.client(); +``` + + + +:::info[Notice for TypeScript] +With the SDK package you get the SplitIO namespace, which contains useful types and interfaces for you to use. + +Feel free to dive into the declaration files if IntelliSense is not enough! +::: + +We recommend instantiating the Split factory once as a singleton and reusing it throughout your application. Consider instantiating it once in the global scope, or in the `componentDidMount` method of your application root component. + +Configure the SDK with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split UI, on your Admin settings page, API keys section. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. + +## Using the SDK + +### Basic use + +When the SDK is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while its in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). + +To make sure the SDK is properly loaded before asking it for a treatment, block until the SDK is ready, as shown below. We set the client to listen for the `SDK_READY` event triggered by the SDK before asking for an evaluation. + +After the `SDK_READY` event fires, you can use the `getTreatment` method to return the proper treatment based on the `FEATURE_FLAG_NAME` and the `key` variables you passed when instantiating the SDK. + +Then use an if-else-if block as shown below and insert the code for the different treatments that you defined in the Split user interface. Remember the final else branch in your code to handle the client returning control. + + + +```javascript +client.on(client.Event.SDK_READY, function() { + var treatment = client.getTreatment("FEATURE_FLAG_NAME"); + + if (treatment == "on") { + // insert code here to show on treatment + } else if (treatment == "off") { + // insert code here to show off treatment + } else { + // insert your control treatment code here + } +}); +``` + + +```typescript +client.on(client.Event.SDK_READY, function() { + const treatment: SplitIO.Treatment = client.getTreatment("FEATURE_FLAG_NAME"); + + if (treatment == "on") { + // insert code here to show on treatment + } else if (treatment == "off") { + // insert code here to show off treatment + } else { + // insert your control treatment code here + } +}); +``` + + + +:::info[Notice when debugging in Android] +When running your app in debug mode on an Android device or emulator, you might get a warning notification stating that *"Setting a timer for a long period of time is a performance and correctness issue on Android"*. + +The warning is explained [here](https://github.com/facebook/react-native/issues/12981#issuecomment-652745831). It is intended to make developers aware that timer callbacks are invoked in foreground, and therefore timers could be delayed while the app is in background. + +Since the SDK uses timers for periodically pushing data to Split cloud, it is acceptable if those operations are delayed while the app is in background, and so it is completely safe to ignore or [hide this warning](https://reactnative.dev/docs/debugging#console-errors-and-warnings). If there is any concern, feel free to contact us through support. + +```javascript +import { LogBox } from 'react-native'; + +LogBox.ignoreLogs(['Setting a timer']); +``` +::: + +### Attribute syntax + +To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK's `getTreatment` method needs to be passed an attribute map at runtime. + +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in the Split user interface to decide whether to show the `on` or `off` treatment to this account. + +The `getTreatment` method has a number of variations that are described below. Each of these additionally has a variation that takes an attributes argument, which can defines attributes of the following types: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: + +* **Strings:** Use type String. +* **Numbers:** Use type Number. +* **Dates: ** Use type Date and express the value in `milliseconds since epoch`.
*Note:* Milliseconds since epoch is expressed in UTC. If your date or date-time combination is in a different timezone, first convert it to UTC, then transform it to milliseconds since epoch. +* **Booleans:** Use type Boolean. +* **Sets:** Use type Array. + + + + +```javascript +var attributes = { + // date attributes are handled as `millis since epoch` + registered_date: new Date('YYYY-MM-DDTHH:mm:ss.sssZ').getTime(), + // this string will be compared against a list called `plan_type` + plan_type: 'growth', + // this number will be compared agains a const value called `deal_size` + deal_size: 10000, + // this boolean will be compared against a const value called `paying_customer` + paying_customer: true, + // this array will be compared against a set called `permissions` + permissions: ["read", "write"] +}; + +var treatment = client.getTreatment('FEATURE_FLAG_NAME', attributes); + +if (treatment === 'on') { + // insert on code here +} else if (treatment === 'off') { + // insert off code here +} else { + // insert control code here +} +``` + + +```typescript +const attributes: SplitIO.Attributes = { + // date attributes are handled as `millis since epoch` + registered_date: new Date('YYYY-MM-DDTHH:mm:ss.sssZ').getTime(), + // this string will be compared against a list called `plan_type` + plan_type: 'growth', + // this number will be compared agains a const value called `deal_size` + deal_size: 10000, + // this array will be compared against a set called `permissions` + permissions: [‘read’, ‘write’] +}; + +const treatment: SplitIO.Treatment = client.getTreatment('FEATURE_FLAG_NAME', attributes); + +if (treatment === 'on') { + // insert on code here +} else if (treatment === 'off') { + // insert off code here +} else { + // insert control code here +} +``` + + + +You can pass your attributes in exactly this way to the `client.getTreatments` method. + +### Binding attributes to the client + +Attributes can optionally be bound to the client at any time during the SDK lifecycle. These attributes are stored in memory and used in every evaluation to avoid the need to keep the attribute set accessible through the whole app. When an evaluation is called, the attributes provided (if any) at evaluation time are combined with the ones that are already loaded into the SDK memory, with the ones provided at function execution time taking precedence. This enables those attributes to be overridden or hidden for specific evaluations. + +An attribute is considered valid if it follows one of the types listed below: +- String +- Number +- Boolean +- Array + +The SDK validates these before storing them and if there are invalid or missing values, possibly indicating an issue, the methods return the boolean `false` and do not update any value. + +To use these methods, refer to the example below: + + + + +```javascript +var attributes = { + registered_date: new Date('YYYY-MM-DDTHH:mm:ss.sssZ').getTime(), + plan_type: 'growth', + deal_size: 10000, + paying_customer: true, + permissions: ["read", "write"] +}; + +// set attributes returns true unless there is an issue storing it +var result = client.setAttributes(attributes); + +// set one attribute and returns true unless there is an issue storing it +var result = client.setAttribute('paying_customer', false); + +// Get an attribute +var plan_type = client.getAttribute('plan_type'); + +// Get all attributes +var stored_attributes = client.getAttributes(); + +// Remove an attribute +var result = client.removeAttribute('permissions'); + +// Remove all attributes +var result = client.clearAttributes(); + +``` + + + +### Multiple evaluations at once + +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` from the Split client to do this. +* `getTreatments`: Pass a list of the feature flag names you want treatments for. +* `getTreatmentsByFlagSet`: Evaluate all flags that are part of the provided set name and are cached on the SDK instance. +* `getTreatmentsByFlagSets`: Evaluate all flags that are part of the provided set names and are cached on the SDK instance. + + + + +```javascript +// Getting treatments by feature flag names +var flagNames = ['FEATURE_FLAG_NAME_1', 'FEATURE_FLAG_NAME_2']; +var treatments = client.getTreatments(flagNames); + +// Getting treatments by set +treatments = client.getTreatmentsByFlagSet('frontend'); + +// Getting treatments for the union of multiple sets +var flagSets = ['frontend', 'client_side']; +treatments = client.getTreatmentsByFlagSets(flagSets); + +// treatments have the following form: +// { +// FEATURE_FLAG_NAME_1: 'on', +// FEATURE_FLAG_NAME_2: 'visa' +// } +``` + + +```typescript +// Getting treatments by feature flag names +const flagNames = ['FEATURE_FLAG_NAME_1', 'FEATURE_FLAG_NAME_2']; +let treatments: SplitIO.Treatments = client.getTreatments(flagNames); + +// Getting treatments by set +treatments = client.getTreatmentsByFlagSet('frontend'); + +// Getting treatments for the union of multiple sets +const flagSets = ['frontend', 'client_side']; +treatments = client.getTreatmentsByFlagSets(flagSets); + +// treatments have the following form: +// { +// FEATURE_FLAG_NAME_1: 'on', +// FEATURE_FLAG_NAME_2: 'visa' +// } +``` + + + +### Get treatments with configurations + +To [leverage dynamic configurations with your treatments](https://help.split.io/hc/en-us/articles/360026943552), you should use the `getTreatmentWithConfig` method. + +This method returns an object with the structure below: + + + + +```javascript +var TreatmentResult = { + String treatment; + String config; // or null if there is no config for the treatment +} +``` + + +```typescript +type TreatmentResult = { + treatment: string, + config: string | null +}; +``` + + + +As you can see from the object structure, the config will be a stringified version of the configuration JSON defined in the Split user interface. If there is no configuration defined for a treatment, the SDK will return `null` for the config parameter. + +This method takes the exact same set of arguments as the standard `getTreatment` method. See below for examples on proper usage: + + + + +```javascript +var treatmentResult = client.getTreatmentWithConfig('FEATURE_FLAG_NAME', attributes); +var configs = JSON.parse(treatmentResult.config); +var treatment = treatmentResult.treatment; + +if (treatment === 'on') { + // insert on code here and use configs here as necessary +} else if (treatment === 'off') { + // insert off code here and use configs here as necessary +} else { + // insert control code here +} +``` + + +```typescript +const treatmentResult: SplitIO.TreatmentWithConfig = client.getTreatmentWithConfig('FEATURE_FLAG_NAME', attributes); +const configs = JSON.parse(treatmentResult.config); +const treatment = treatmentResult.treatment; + +if (treatment === 'on') { + // insert on code here and use configs here as necessary +} else if (treatment === 'off') { + // insert off code here and use configs here as necessary +} else { + // insert control code here +} +``` + + + +If you need to get multiple evaluations at once, you can also use the `getTreatmentsWithConfig` methods. These methods take the exact same arguments as the [getTreatments](#multiple-evaluations-at-once) methods but return a mapping of feature flag names to TreatmentResults instead of strings. Example usage below: + + + + +```javascript +// Getting treatments by feature flag names +var featureFlagNames = ['FEATURE_FLAG_NAME_1', 'FEATURE_FLAG_NAME_2']; + +var treatmentResults = client.getTreatmentsWithConfig(featureFlagNames); + +// Getting treatments by set +treatmentResults = client.getTreatmentsWithConfigByFlagSet('frontend'); + +// Getting treatments for the union of multiple sets +var flagSets = ['frontend', 'client_side']; +treatmentResults = client.getTreatmentsWithConfigByFlagSets(flagSets); + +// treatmentResults will have the following form: +// { +// FEATURE_FLAG_NAME_1: {treatment: 'on', +// config: "{ 'color' : 'red'}}", +// FEATURE_FLAG_NAME_2: {treatment: 'v2', +// config: "{ 'copy' : 'better copy'}}", +// } +``` + + +```typescript +// Getting treatments by feature flag names +const featureFlagNames = ['FEATURE_FLAG_NAME_1', 'FEATURE_FLAG_NAME_2']; + +const treatmentResults: SplitIO.TreatmentsWithConfig = client.getTreatmentsWithConfig(featureFlagNames); + +// Getting treatments by set +treatmentResults = client.getTreatmentsWithConfigByFlagSet('frontend'); + +// Getting treatments for the union of multiple sets +const flagSets = ['frontend', 'client_side']; +treatmentResults = client.getTreatmentsWithConfigByFlagSets(flagSets); + +// treatmentResults have the following form: +// { +// FEATURE_FLAG_NAME_1: {treatment: 'on', +// config: "{ 'color' : 'red'}}", +// FEATURE_FLAG_NAME_2: {treatment: 'v2', +// config: "{ 'copy' : 'better copy'}}", +// } +``` + + + +### Shutdown + +You can call the `client.destroy()` method to gracefully shut down the Split SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. +If the SDK was instantiated in the `componentDidMount` method of a React component, `destroy` should be called in the corresponding `componentWillUnmount` method. + +However while releasing resources if the SDK is not needed anymore is a good practice, since the SDK automatically hooks to application state transitions (foreground, background) data synchronization is managed by the SDK and pending events are flushed automatically. + + + + +```javascript +// You can just destroy and remove the variable reference and move on: +user_client.destroy(); +user_client = null; + +// destroy() returns a promise, so if you want to, for example, +// navigate to another page without losing impressions, you +// can do that once the promise resolves. +user_client.destroy().then(function() { + user_client = null; + + document.location.replace('another_page'); +}); +``` + + + +After `destroy()` is called and finishes, any subsequent invocations to `getTreatment`/`getTreatments` or manager methods result in `control` or empty list, respectively. + +:::warning[Important!] +A call to the `destroy()` method also destroys the factory object. When creating new client instance, first create a new factory instance. +::: + +## Track + +Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users’ actions and metrics. + +[Learn more](https://help.split.io/hc/en-us/articles/360020585772) about using track events in feature flags. + +In the examples below you can see that the `.track()` method can take up to four arguments. The proper data type and syntax for each are: + +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: + * Contains 63 characters or fewer. + * Starts with a letter or number. + * Contains only letters, numbers, hyphen, underscore, or period. + * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` +* **VALUE:** (Optional) The value to be used in creating the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is **Integer** or **Float**. +* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. + +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event to be sent back to Split's servers on the next event post. The SDK returns `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method has been provided. + +In the case that a bad input has been provided, you can read more about our SDK's expected behavior [here](https://help.split.io/hc/en-us/articles/360020585772-Track-events) + + + + +```javascript +// The expected parameters are: +var queued = client.track('TRAFFIC_TYPE', 'EVENT_TYPE', eventValue, { properties }); + +// Example with both a value and properties +var properties = {package : "premium", admin : true, discount : 50}; +var queued = client.track('user', 'page_load_time', 83.334, properties); + +// Example with only properties +var properties = {package : "premium", admin : true, discount : 50}; +var queued = client.track('user', 'page_load_time', null, properties); +``` + + +```typescript +// The expected parameters are: +const queued: boolean = client.track('TRAFFIC_TYPE', 'EVENT_TYPE', eventValue, , { properties }); + +// Example with both a value and properties +const properties = {package : "premium", admin : true, discount : 50}; +const queued = client.track('user', 'page_load_time', 83.334, properties); + +// Example with only properties +const properties = {package : "premium", admin : true, discount : 50}; +const queued = client.track('user', 'page_load_time', null, properties); +``` + + + +## Configuration + +The SDK has a number of knobs for configuring performance. Each knob is tuned to a reasonable default. However, you can override the value while instantiating the SDK. The parameters available for configuration are shown below. + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| core.labelsEnabled | Enable impression labels from being sent to Split's backend. Labels may contain sensitive information. | true | +| startup.readyTimeout | Maximum amount of time in seconds to wait before firing the `SDK_READY_TIMED_OUT` event | 10 | +| startup.requestTimeoutBeforeReady | The SDK has two main endpoints it uses /splitChanges and /mySegments that it hits to get ready. This config sets how long (in seconds) the SDK will wait for each request it makes as part of getting ready. | 5 | +| startup.retriesOnFailureBeforeReady | How many retries on /splitChanges and /mySegments we will do while getting the SDK ready | 1 | +| startup.eventsFirstPushWindow | Use to set a specific timer (expressed in seconds) for the first push of events, starting on SDK initialization. | 10 | +| scheduler.featuresRefreshRate | The SDK polls Split servers for changes to feature rollout plans. This parameter controls this polling period in seconds. | 60 | +| scheduler.segmentsRefreshRate | The SDK polls Split servers for changes to segment definitions. This parameter controls this polling period in seconds. | 60 | +| scheduler.impressionsRefreshRate | The SDK sends information on who got what treatment at what time back to Split servers to power analytics. This parameter controls how often this data is sent to Split servers. The parameter should be in seconds. | 300 | +| scheduler.impressionsQueueSize | The max amount of impressions we queue. If the queue is full, the SDK flushes the impressions and resets the timer. | 30000 | +| scheduler.eventsPushRate | The SDK sends tracked events to Split servers. This setting controls that flushing rate in seconds. | 60 | +| scheduler.eventsQueueSize | The max amount of events we queue. If the queue is full, the SDK flushes the events and resets the timer. | 500 | +| scheduler.telemetryRefreshRate | The SDK caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers (in seconds). | 3600 seconds (1 hour) | +| sync.splitFilters | Filter specific feature flags to be synced and evaluated by the SDK. This is formed by a type string property and a list of string values for the given criteria. Using the types 'bySet' (recommended, flag sets are available in all tiers) or 'byName', pass an array of strings defining the query. If empty or unset, all feature flags are downloaded by the SDK. | [] | +| sync.impressionsMode | This configuration defines how impressions (decisioning events) are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Split; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, ALL impressions are queued and sent to Split; this is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. | `OPTIMIZED` | +| sync.enabled | Controls the SDK continuous synchronization flags. When `true`, a running SDK processes rollout plan updates performed in the Split user interface (default). When `false`, it fetches all data upon init, which ensures a consistent experience during a user session and optimizes resources when these updates are not consumed by the app. | true | +| sync.requestOptions.getHeaderOverrides | A callback function that can be used to override the Authentication header or append new headers to the SDK's HTTP(S) requests. | undefined | +| debug | Either a boolean flag, string log level or logger instance for activating SDK logs. See [logging](#logging) for details. | false | +| streamingEnabled | Boolean flag to enable the streaming service as default synchronization mechanism. In the event of an issue with streaming, the SDK will fallback to the polling mechanism. If false, the SDK will poll for changes as usual without attempting to use streaming. | true | +| userConsent | User consent status used to control the tracking of events and impressions. Possible values are `GRANTED`, `DECLINED`, and `UNKNOWN`. See [User consent](#user-consent) for details. | `GRANTED` | + +To set each of the parameters defined above, use the following syntax. + + + + +```javascript +var sdk = SplitFactory({ + startup: { + readyTimeout: 10, // 10 sec + eventsFirstPushWindow: 10 // 10 sec + }, + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'YOUR_KEY' + }, + scheduler: { + featuresRefreshRate: 5, // 5 sec + segmentsRefreshRate: 60, // 60 sec + impressionsRefreshRate: 300, // 300 sec + impressionsQueueSize: 30000, // 30000 items + eventsPushRate: 60, // 60 sec + eventsQueueSize: 500, // 500 items + telemetryRefreshRate: 3600 // 1 hour + }, + sync: { + splitFilters: [{ + type: 'bySet', + values: ['frontend'] + }], + impressionsMode: 'OPTIMIZED' + }, + streamingEnabled: true, + debug: false +}); +``` + + +```typescript +const sdk: SplitIO.IBrowserSDK = SplitFactory({ + startup: { + readyTimeout: 10, // 10 sec + eventsFirstPushWindow: 10 // 10 sec + }, + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'YOUR_KEY' + }, + scheduler: { + featuresRefreshRate: 5, // 5 sec + segmentsRefreshRate: 60, // 60 sec + impressionsRefreshRate: 300, // 300 sec + impressionsQueueSize: 30000, // 30000 items + eventsPushRate: 60, // 60 sec + eventsQueueSize: 500, // 500 items + telemetryRefreshRate: 3600 // 1 hour + }, + sync: { + splitFilters: [{ + type: 'bySet', + values: ['frontend'] + }], + impressionsMode: 'OPTIMIZED' + }, + streamingEnabled: true, + debug: false +}); +``` + + + +## Localhost mode + +For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the Split SDK can be started in **localhost** mode (aka off-the-grid or offline mode). In this mode, the SDK neither polls nor updates Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the features. + +Define the feature flags you want to use in the `features` object map. All `getTreatment` calls for a feature flag now only return the one treatment (and config, if defined) that you have defined in the map. You can then change the treatment as necessary for your testing. To update a treatment or a config, or to add or remove feature flags from the mock cache, update the properties of the `features` object you've provided. The SDK simulates polling for changes and updates from it. Do not assign a new object to the `features` property because the SDK has a reference to the original object and will not detect the change. + +Any feature that is not provided in the `features` map returns the [control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment) if the SDK was asked to evaluate them. + +You can use the additional configuration parameters below when instantiating the SDK in `localhost` mode. + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| scheduler.offlineRefreshRate | The refresh interval for the mocked features treatments. | 15 | +| features | A fixed mapping of which treatment to show for our mocked features. | {}
By default we have no mocked features. | + +To use the SDK in localhost mode, replace the SDK Key on `authorizationKey` property with `'localhost'`, as shown in the example below. Note that you can define in the `features` object a feature flag name and its treatment directly or use a map to define both a treatment and a dynamic configuration. + +If you define just a string as the value for a feature flag name, any config returned by our SDKs will always be null. If you use a map, we return the specified treatment and the specified config (which can also be null). + + + +```javascript +import { SplitFactory } from '@splitsoftware/splitio-react-native'; + +var sdk = SplitFactory({ + core: { + authorizationKey: 'localhost' + }, + features: { + 'reporting_v2': 'on', // example with just a string value for the treatment + 'billing_updates': { treatment: 'visa', config: '{ "color": "blue" }' }, // example of a defined config + 'show_status_bar': { treatment: 'off', config: null } // example of a null config + }, + scheduler: { + offlineRefreshRate: 15 // 15 sec + } +}); + +var client = sdk.client(); + +// The following code will be evaluated once the engine finishes the initialization +client.on(client.Event.SDK_READY, function() { + // The sentence below will return 'on' + var t1 = client.getTreatment('reporting_v2') + // The sentence below will return an object with the structure of: {treatment:'visa',config:'{ "color":"blue" }'} + var t2 = client.getTreatmentWithConfig('billing_updates') + // The sentence below will return 'control' because that feature does not exist + var t3 = client.getTreatmentWithConfig('navigation_bar_changes') +}); +``` + + +```typescript +import { SplitFactory } from '@splitsoftware/splitio-react-native'; + +const sdk: SplitIO.IBrowserSDK = SplitFactory({ + core: { + authorizationKey: 'localhost' + }, + features: { + 'reporting_v2': 'on', // example with just a string value for the treatment + 'billing_updates': { treatment: 'visa', config: '{ "color": "blue"}' } // example of a defined config + 'show_status_bar': { treatment: 'off', config: null } // example of a null config + }, + scheduler: { + offlineRefreshRate: 15 // 15 sec + } +}); + +const client: SplitIO.IBrowserClient = sdk.client(); + +// The following code will be evaluated once the engine finishes the initialization +client.on(client.Event.SDK_READY, () => { + // The sentence below will return 'on' + const t1: SplitIO.Treatment = client.getTreatment('reporting_v2'); + // The sentence below will return an object with the structure of: {treatment:'visa',config:'{ "color":"blue" }' + const t2: SplitIO.Treatment = client.getTreatmentWithConfig('billing_updates'); + // The sentence below will return 'control' because that feature does not exist + const t3: SplitIO.Treatment = client.getTreatmentWithConfig('navigation_bar_changes'); +}); +``` + + + +:::info[Testing with Jest] + +We recommend using the SDK in localhost mode for your tests. + +For example, you can mock the module import (see [Jest documentation](https://jestjs.io/docs/jest-object#jestmockmodulename-factory-options) for details) to instantiate the SDK in localhost mode as shown below: + +```javascript +jest.mock('@splitsoftware/splitio-react-native', () => { + const splitio = jest.requireActual('@splitsoftware/splitio-react-native'); + return { + ...splitio, + SplitFactory: () => { + return splitio.SplitFactory({ + core: { + authorizationKey: 'localhost', + }, + // Mock your feature flags and treatments here + features: { + feature_flag_1: 'on', + } + }); + }, + }; +}); +``` + +It is not recommended to use the default (online) mode of the SDK in your tests because it slows them down and increases their instability due to network latencies. However, if you must use it, you need to polyfill the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), which is used by the SDK but not provided by Jest and Node.js. [`isomorphic-fetch`](https://www.npmjs.com/package/isomorphic-fetch) is a good option for that. +::: + +## Manager + +Use the Split Manager to get a list of features available to the Split client. + +To instantiate a Manager in your code base, use the same factory that you used for your client. + + + + +```javascript +var factory = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + // the key can be the logged in + // user id, or the account id that + // the logged in user belongs to. + // The type of customer (user, account, custom) + // is chosen during Split's sign-up process. + key: 'key' + } +}); + +var manager = factory.manager(); + +manager.once(manager.Event.SDK_READY, function() { + // Once it's ready, use the manager +}); +``` + + +```typescript +const factory: SplitIO.IBrowserSDK = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + // the key can be the logged in + // user id, or the account id that + // the logged in user belongs to. + // The type of customer (user, account, custom) + // is chosen during Split's sign-up process. + key: 'key' + } +}); + +const manager: SplitIO.IManager = factory.manager(); + +manager.once(manager.Event.SDK_READY, function() { + // Once it's ready, use the manager +}); +``` + + + +The Manager then has the following methods available. + + + + +```javascript +/** + * Returns the feature flag registered with the SDK of this name. + * + * @return SplitView or null. + */ +var splitView = manager.split('name-of-feature-flag'); + +/** + * Retrieves the feature flags that are currently registered with the + * SDK. + * + * returns a List of SplitViews. + */ +var splitViewsList = manager.splits(); + +/** + * Returns the names of feature flags registered with the SDK. + * + * @return a List of Strings of the feature flag names. + */ +var splitNamesList = manager.names(); +``` + + +```typescript +/** + * Returns the feature flag registered with the SDK of this name. + * + * @return SplitView or null. + */ +const splitView: SplitIO.SplitView = manager.split('name-of-feature-flag'); + +/** + * Retrieves the feature flags that are currently registered with the + * SDK. + * + * returns a List of SplitViews. + */ +const splitViewsList: SplitIO.SplitViews = manager.splits(); + +/** + * Returns the names of feature flags registered with the SDK. + * + * @return a List of Strings of the feature flag names. + */ +const splitNamesList: SplitIO.SplitNames = manager.names(); +``` + + + +The `SplitView` object referenced above has the following structure: + +```typescript title="TypeScript" +type SplitView = { + name: string, + trafficType: string, + killed: boolean, + treatments: Array, + changeNumber: number, + configs: { + [treatmentName: string]: string + }, + defaultTreatment: string, + sets: Array, + impressionsDisabled: boolean +} +``` + +## Listener + +Split SDKs send impression data back to Split servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression listener*. For that purpose, the SDK's configurations have a parameter called `impressionListener` where an implementation of `ImpressionListener` could be added. This implementation **must** define the `logImpression` method and it receives data in the following schema. + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| impression | Object | Impression object that has the feature, key, treatment, label, etc. | +| attributes | Object | A map of attributes passed to `getTreatment`/`getTreatments` (if any). | +| sdkLanguageVersion | String| The version of the SDK. In this case the language is `reactnative` plus the version currently running. | + +:::info[Note] +There are two additional keys on this object, `ip` and `hostname`. They are not captured on the client side but kept for consistency. +::: + +## Implement custom impression listener + +Here is an example of how implement a custom impression listener. + + + +```javascript +function logImpression(impressionData) { + // do something with the impression data. +} + +var factory = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + impressionListener: { + logImpression: logImpression + } +}); +``` + + +```typescript +class MyImprListener implements SplitIO.IImpressionListener { + logImpression(impressionData: SplitIO.ImpressionData) { + // do something with impressionData + } +} + +const factory: SplitIO.IBrowserSDK = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + impressionListener: { + logImpression: new MyImprListener() + } +}); +``` + + + +An impression listener is called asynchronously from the corresponding evaluation, but is almost immediate. + +Even though the SDK does not fail if there is an exception in the listener, do not block the call stack. + +## Logging + +To trim as many bits as possible from the user application builds, we divided the logger in implementations that contain the log messages for each log level: `ErrorLogger`, `WarnLogger`, `InfoLogger`, and `DebugLogger`. Higher log level options contain the messages for the lower ones, with DebugLogger containing them all. +Thus, to enable descriptive SDK logging you need to plug in a logger instance as shown below: + + + +```javascript +import { SplitFactory, DebugLogger } from '@splitsoftware/splitio-react-native'; + +const sdk: SplitIO.IBrowserSDK = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + debug: DebugLogger() // other options are `InfoLogger`, `WarnLogger` and `ErrorLogger` +}); +``` + + +```javascript +var splitio = require('@splitsoftware/splitio-react-native'); + +var sdk = splitio.SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + debug: splitio.DebugLogger() // other options are `InfoLogger`, `WarnLogger` and `ErrorLogger` +}); +``` + + + +You can also enable the SDK logging via a boolean or log level value as `debug` settings, and change it dynamically by calling the SDK Logger API. + +However, in any case where the proper logger instance is not plugged in, instead of a human readable message you'll get a code and optionally some params for the log itself. +While these logs would be enough for the Split support team, if you find yourself in a scenario where you need to parse this information, you can check the constant files in our javascript-commons repository (where you have tags per version if needed) under the [logger folder](https://github.com/splitio/javascript-commons/blob/master/src/logger/). + + + + +```javascript +import { SplitFactory } from '@splitsoftware/splitio-react-native'; + +const sdk: SplitIO.IBrowserSDK = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + debug: true // other options are 'ERROR', 'WARN', 'INFO' and 'DEBUG +}); + +// Or you can use the Logger API methods which have an immediate effect. +sdk.Logger.setLogLevel('WARN'); // Acceptable values are: 'DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE' +sdk.Logger.enable(); // equivalent to `setLogLevel('DEBUG')` +sdk.Logger.disable(); // equivalent to `setLogLevel('NONE')` +``` + + +```javascript +var splitio = require('@splitsoftware/splitio-react-native'); + +var sdk = splitio.SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + debug: true // other options are 'ERROR', 'WARN', 'INFO' and 'DEBUG +}); + +// Or you can use the Logger API methods which have an immediate effect. +sdk.Logger.setLogLevel('WARN'); // Acceptable values are: 'DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE' +sdk.Logger.enable(); // equivalent to `setLogLevel('DEBUG')` +sdk.Logger.disable(); // equivalent to `setLogLevel('NONE')` +``` + + + +## Advanced use cases + +This section describes advanced use cases and features provided by the SDK. + +### Instantiate multiple SDK clients + +Split supports the ability to release based on multiple traffic types. For example, with traffic types, you can release to `users` in one feature flag and `accounts` in another. If you are unfamiliar with using multiple traffic types, refer to the [Traffic type guide](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) for more information. + +Each SDK client is tied to one specific customer ID at a time, so if you need to roll out feature flags by different traffic types, instantiate multiple SDK clients, one for each traffic type. For example, you may want to roll out the feature `user-poll` by `users` and the feature `account-permissioning` by `accounts`. + +You can do this with the example below. + + + + +```javascript +var factory = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'CUSTOMER_ACCOUNT_ID', + // Instantiate the sdk once and provide the ID for one of the + // traffic types that you plan to release to. It doesn't + // matter which you pick to start off with. + }, +}); + +// When you call factory.client(), the sdk will create a client +// using the Account ID passed in during the factory creation in the `key` field. +var account_client = factory.client(); + +// To create another client for a User instead, just pass in a User ID (of traffic type user). +// Note: this is only valid after at least one client has been initialized. +var user_client = factory.client('CUSTOMER_USER_ID'); + +// check treatment for user-poll and CUSTOMER_USER_ID +var user_poll_treatment = user_client.getTreatment('user-poll'); + +// check treatment for account-permissioning and CUSTOMER_ACCOUNT_ID +var account_permissioning_treatment = account_client.getTreatment('account-permissioning'); + +// track events for accounts +user_client.track('account', 'PAGELOAD', 7.86); + +// or track events for users +account_client.track('user', 'ACCOUNT_CREATED'); +``` + + +```typescript +const sdk: SplitIO.IBrowserSDK = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'CUSTOMER_ACCOUNT_ID' + // Instantiate the sdk once and provide the ID for one of the + // traffic types that you plan to release to. It doesn't + // matter which you pick to start off with. + }, +}); + +// When you call factory.client(), the sdk will create a client +// using the Account ID passed in during the factory creation in the `key` field. +const account_client: SplitIO.IBrowserClient = factory.client(); + +// To create another client for a User instead, just pass in a User ID (of traffic type user). +// Note: this is only valid after at least one client has been initialized. +const user_client: SplitIO.IBrowserClient = factory.client('CUSTOMER_USER_ID'); + +// check treatment for user-poll and CUSTOMER_USER_ID +const user_poll_treatment: SplitIO.Treatment = + user_client.getTreatment('user-poll'); + +// check treatment for account-permissioning and CUSTOMER_ACCOUNT_ID +const account_permissioning_treatment: SplitIO.Treatment = + account_client.getTreatment('account-permissioning'); + +// track events for accounts +user_client.track('account', 'PAGELOAD', 7.86); + +// or track events for users +account_client.track('user', 'ACCOUNT_CREATED'); +``` + + + +:::info[Number of SDK instances] +While the SDK does not put any limitations on the number of instances that can be created, we strongly recommend keeping the number of SDKs down to **one** or **two**. +::: + +### Subscribe to events + +You can listen for three different events from the SDK. + +* `SDK_READY`. This event fires once the SDK is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Split servers. +* `SDK_READY_TIMED_OUT`. This event fires if the SDK could not download the data from Split servers within the time specified by the `readyTimeout` configuration parameter. This event does not indicate that the SDK initialization was interrupted. The SDK continues downloading the rollout plan and fires the `SDK_READY` event when finished. This delayed `SDK_READY` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. +* `SDK_UPDATE`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in the Split user interface. + +The syntax to listen for each event is shown below. + + + + +```javascript +function whenReady() { + var treatment = client.getTreatment('YOUR_FEATURE_FLAG'); + + if (treatment === 'on') { + // insert on code + } else if (treatment === 'off') { + // insert off code + } else { + // insert control code (usually the same as default treatment) + } +} + +// the client is ready for start making evaluations with your data +client.once(client.Event.SDK_READY, whenReady); + +client.once(client.Event.SDK_READY_TIMED_OUT, function () { + // this callback will be called after 1.5 seconds if and only if the client + // is not ready for that time. You can still call getTreatment() + // but it could return CONTROL. +}); + +client.on(client.Event.SDK_UPDATE, function () { + // fired each time the client state change. + // For example, when a feature flag or segment changes. + console.log('The SDK has been updated!'); +}); +``` + + +```typescript +function whenReady() { + const treatment: SplitIO.Treatment = client.getTreatment('YOUR_FEATURE_FLAG'); + + if (treatment === 'on') { + // insert on code + } else if (treatment === 'off') { + // insert off code + } else { + // insert control code (usually the same as default treatment) + } +} + +// the client is ready for start making evaluations with your data +client.once(client.Event.SDK_READY, whenReady); + +client.once(client.Event.SDK_READY_TIMED_OUT, () => { + // this callback will be called after 1.5 seconds if and only if the client + // is not ready for that time. You can still call getTreatment() + // but it could return CONTROL. +}); + +client.on(client.Event.SDK_UPDATE, () => { + // fired each time the client state change. + // For example, when a feature flag or segment changes. + console.log('The SDK has been updated!'); +}); +``` + + + +### User consent + +The SDK allows you to disable the tracking of events and impressions until user consent is explicitly granted or declined. + +The `userConsent` configuration parameter lets you set the initial consent status of the SDK instance, and the factory method `UserConsent.setStatus(boolean)` lets you grant (enable) or decline (disable) dynamic data tracking. + +There are three possible initial states: + * `'GRANTED'`: The user grants consent for tracking events and impressions. The SDK sends them to Split cloud. This is the default value if `userConsent` param is not defined. + * `'DECLINED'`: The user declines consent for tracking events and impressions. The SDK does not send them to Split cloud. + * `'UNKNOWN'`: The user neither grants nor declines consent for tracking events and impressions. The SDK tracks them in its internal storage, and eventually either sends them or not if the consent status is updated to `'GRANTED'` or `'DECLINED'` respectively. + +The status can be updated at any time with the `UserConsent.setStatus` factory method. + +Working with user consent is demonstrated below. + +```javascript title="User consent: Initial config, getter and setter" +var factory = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + // Overwrites the initial consent status of the factory instance, which is 'GRANTED' by default. + // 'UNKNOWN' status represents that the user has neither granted nor declined consent for tracking data, + // so the SDK will locally track data but not send it to Split cloud until consent is changed to 'GRANTED'. + userConsent: 'UNKNOWN' +}); + +// `getStatus` method returns the current consent status. +factory.UserConsent.getStatus() === factory.UserConsent.Status.UNKNOWN; + +// `setStatus` method lets you update the factory consent status at any time. +// Pass `true` for 'GRANTED' and `false` for 'DECLINED'. +factory.UserConsent.setStatus(true); // Consent status changed from 'UNKNOWN' to 'GRANTED'. Data will be sent to Split cloud. +factory.UserConsent.getStatus() === factory.UserConsent.Status.GRANTED; + +factory.UserConsent.setStatus(false); // Consent status changed from 'GRANTED' to 'DECLINED'. Data will not be sent to Split cloud. +factory.UserConsent.getStatus() === factory.UserConsent.Status.DECLINED; + +``` + +### Usage with React SDK + +The [Split React SDK](https://help.split.io/hc/en-us/articles/360038825091-React-SDK) is a wrapper around the Split JavaScript SDK that provides a more React-friendly API based on React components and hooks. You can use the React Native SDK with the React SDK in your React Native application following this [Usage Guide](https://help.split.io/hc/en-us/articles/360038825091-React-SDK#usage-with-react-native). + +## Example apps + +Here is an example application detailing how to configure and instantiate the Split React Native SDK. + +* [React Native & Expo examples](https://github.com/splitio/react-native-sdk-example) diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/react-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/react-sdk.md new file mode 100644 index 00000000000..125e7b57fa9 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/react-sdk.md @@ -0,0 +1,1426 @@ +--- +title: React SDK +sidebar_label: React SDK +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +This guide provides detailed information about our React SDK. This library is built on top of our regular [JavaScript SDK](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK) to ease the integration in React applications by providing a set of components and custom hooks based on the React Hooks API, so you can interact with the underneath SDK and work towards any use cases. All of our SDKs are open source. Go to our [React SDK GitHub repository](https://github.com/splitio/react-client) to see the source code. + +:::info[Migrating from v1.x to v2.x] +Refer to the [migration guide](https://github.com/splitio/react-client/blob/development/MIGRATION-GUIDE.md) for information on upgrading to v2.x. +::: + +:::info[Deprecated HOCs & components] +High-Order-Components (`withSplitFactory`, `withSplitClient`, and `withSplitTreatments`) and components that accept a render function as child component (`SplitTreatments` and `SplitClient`) have been deprecated and might be removed in a future major release. + +The deprecation is intended to simplify the API and discourage using old patterns (HOCs and render props) in favor of the *hook* alternatives, to take advantage of React optimizations. + +Although these components are still working, we recommend migrating to the `SplitFactoryProvider` component and library hooks. Refer to the [migration guide](https://github.com/splitio/react-client/blob/development/MIGRATION-GUIDE.md#migrating-to-react-sdk-v200) for more details. +::: + +## Language support + +The Split React SDK requires React 16.8.0 or above, since it uses **React Hooks API** introduced in that version. + +The SDK supports all major web browsers. It was built to support ES5 syntax but it depends on native support for ES6 `Promise`, `Map`, and `Set` objects, so these objects need to be **polyfilled** if they are not available in your target browsers, like IE 11. + +If you're looking for possible polyfill options, check [es6-promise](https://github.com/stefanpenner/es6-promise), [es6-map](https://github.com/medikoo/es6-map) and [es6-set](https://github.com/medikoo/es6-set) for `Promise`, `Map` and `Set` polyfills respectively. + +## Initialization + +Set up Split in your code base in two steps. + +### 1. Import the SDK into your project + +You can import the SDK into your project using one of the following three methods: + + + +```bash +npm install --save @splitsoftware/splitio-react +``` + + +```bash +yarn add @splitsoftware/splitio-react +``` + + +```html + + +``` + + + +### 2. Instantiate the SDK and create a new Split client + +The code examples below show how to instantiate the SDK. The code creates a Split factory, which begins downloading your rollout plan so you can evaluate feature flags, and creates a client. + + + + +```javascript +// CDN will expose the library globally with the "splitio" variable. Use the options that work best with your code. +const { SplitFactoryProvider } = window.splitio; + +// Create the config for the SDK factory. +const sdkConfig = { + core: { + authorizationKey: 'YOUR_CLIENT_SIDE_SDK_KEY', + // key represents your internal user id, or the account id that + // the user belongs to. + // This could also be a cookie you generate for anonymous users. + key: 'key' + } +}; + +// Using the SplitFactoryProvider component, MyApp component and all its descendants have access to the SDK functionality. +const App = () => ( + + + +); +``` + + +```javascript +// The npm package exposes the different components and functions of the library as named exports. +const { SplitFactoryProvider } = require('@splitsoftware/splitio-react'); + +// Create the config for the SDK factory. +const sdkConfig = { + core: { + authorizationKey: 'YOUR_CLIENT_SIDE_SDK_KEY', + // key represents your internal user id, or the account id that + // the user belongs to. + // This could also be a cookie you generate for anonymous users. + key: 'key' + } +}; + +// Using the SplitFactoryProvider component, MyApp component and all its descendants have access to the SDK functionality. +const App = () => ( + + + +); +``` + + +```javascript +// The npm package exposes the different components and functions of the library as named exports. +import { SplitFactoryProvider } from '@splitsoftware/splitio-react'; + +// Create the config for the SDK factory. +const sdkConfig: SplitIO.IBrowserSettings = { + core: { + authorizationKey: 'YOUR_CLIENT_SIDE_SDK_KEY', + // key represents your internal user id, or the account id that + // the user belongs to. + // This could also be a cookie you generate for anonymous users. + key: 'key' + } +}; + +// Using the SplitFactoryProvider component, MyApp component and all its descendants have access to the SDK functionality. +const App: React.ComponentType = () => ( + + + +); +``` + + + +**For more flexibility** as we wanted to support most use cases, the `SplitFactoryProvider` component can receive the factory already instantiated for our React SDK to use it. If you're using the JavaScript SDK already, it is recommended that you follow the Singleton Pattern and keep only one instance of the factory at all times, which you can provide to the React SDK following the approach shown below. + +You could access the JavasScript SDK factory function through the `SplitFactory` named export of the React SDK too, which points to the original JavaScript SDK function so you don't need to import two different Split packages. + +**Note that you shouldn't mix the two options**. Either provide a config or a factory instance. If both are provided, config will be ignored and you'll get a warning log. + +When using the **`config`** prop, the `SplitFactoryProvider` component automatically instantiates and shuts down the SDK factory when the component is mounted and unmounted respectively. + +Therefore, you should use the `config` prop for simpler setups where you know that the component [state in the render tree](https://react.dev/learn/preserving-and-resetting-state) will not reset, i.e., it will not be unmounted and mounted again (for instance, when you have a single `SplitFactoryProvider` component in the root or near the root of your app). Otherwise, a new factory instance will be created (if you pass a new `config` object) or the factory will be re-initialized (if you pass a reference to the same `config` object and the component is mounted again). + +When using the **`factory`** prop, the `SplitFactoryProvider` component doesn't shut down the SDK automatically. You should handle the [shutdown](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#shutdown) yourself. + +You can use it for more complex setups. For example, when you need to use the `SplitFactoryProvider` component in different parts of your app, or when the `SplitFactoryProvider` is nested in a component that might be unmounted and mounted again, for example in a tabbed view, or a micro-frontend architecture. In these cases, you can create the factory instance as a global variable, and pass it down to the `SplitFactoryProvider` components. + +```javascript title="TypeScript" +// The npm package exposes the different components and functions of the library as named exports. +// If you were using the bundle via CDN, it'll be at `window.splitio.SplitFactory` +import { SplitFactory, SplitFactoryProvider } from '@splitsoftware/splitio-react'; + +// Create the Split factory object with your custom settings, using the re-exported function. +const factory: SplitIO.IBrowserSDK = SplitFactory({ + core: { + authorizationKey: 'YOUR_CLIENT_SIDE_SDK_KEY', + key: 'key' + }, + ... +}); + +// Using the SplitFactoryProvider component, MyApp component and all it's descendants will have access to the SDK functionality using the provided factory. +const App: React.ComponentType = () => ( + + + +); +``` + +:::info[Notice for TypeScript] +With the SDK package on NPM, you get the SplitIO namespace, which contains useful types and interfaces for you to use. + +Feel free to dive into the declaration files if IntelliSense is not enough! +::: + +We recommend instantiating the Split factory once as a singleton and reusing it throughout your application. + +Configure the SDK with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split UI, on your Admin settings page, API keys section. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. + +## Using the SDK + +### Get treatments with configurations + +When the SDK is instantiated, it kicks off background tasks to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while its in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). + +To make sure the SDK is properly loaded before asking it for a treatment, block until the SDK is ready, as shown below. We provide the `isReady` boolean prop based on the client that will be used by the component. Internally we listen for the `SDK_READY` event triggered by given SDK client to set the value of `isReady`. + +After the `isReady` prop is set to true, you can use the SDK. The `useSplitTreatments` hook returns the proper treatments based on the `names` prop value passed to it and the `core.key` value you passed in the config when instantiating the SDK. Then use the `treatments` property to access the treatment values as well as the corresponding [dynamic configurations](https://help.split.io/hc/en-us/articles/360026943552) that you defined in the Split user interface. Remember to handle the client returning control as a safeguard. + +Similarly to the vanilla JS SDK, React SDK supports the ability to evaluate flags based on cached content when using [LOCALSTORAGE](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#configuration) as storage type. In this case, the `isReadyFromCache` prop will change to true almost instantly since access to the cache is synchronous, allowing you to consume flags earlier on components that are critical to your UI. Keep in mind that the data might be stale until `isReady` prop is true. Read more [below](#subscribe-to-events-and-changes). + + + +```javascript +import { useSplitTreatments } from '@splitsoftware/splitio-react'; +import MyComponentV1 from './MyComponentV1'; +import MyComponentV2 from './MyComponentV2'; + +const featureName = 'FEATURE_FLAG_NAME'; + +function renderContent(treatmentWithConfig, props) { + const { treatment, config } = treatmentWithConfig; + + if (treatment === 'on') return (); + + return (); +} + +/** + * This is another way to write a toggler with a function component that uses hooks. + **/ +function MyComponentToggle (props) { + + // evaluate feature flags with the `useSplitTreatments` hook + const { treatments, isReady } = useSplitTreatments({ names: [featureName] }); + + return isReady ? + renderContent(treatments[featureName], props) : // Use the treatments and configs. + ; // Render a spinner if the SDK is not ready yet. +} + +export default MyComponentToggle; +``` + + +```javascript +import { SplitTreatments } from '@splitsoftware/splitio-react'; +import MyComponentV1 from './MyComponentV1'; +import MyComponentV2 from './MyComponentV2'; + +const featureName = 'FEATURE_FLAG_NAME'; + +/** + * This is one way to write a toggler component, which might be convenient for code cleanup afterwards + * as you remove both toggle and legacy component, then wire the version you'll keep. + * But it's not the only way to use the treatments. Always follow the pattern that works best for you! + **/ +export default class MyComponentToggle extends React.Component { + + renderContent(treatmentWithConfig) { + const { treatment, config } = treatmentWithConfig; + + if (treatment === 'on') return (); + + return (); + } + + render() { + return ( + + {({ treatments, isReady }) => { // Passes down a TreatmentsWithConfig object and SplitContext properties like the boolean `isReady` flag. + return isReady ? + this.renderContent(treatments[featureName]) : // Use the treatments and configs. + ; // Render a spinner if the SDK is not ready yet. You can do what you want with this boolean. + }} + + ); + } +} +``` + + + +The treatments property/value returned by this library has the following shape: + +```javascript title="TypeScript" +type TreatmentWithConfig = { + treatment: string, + config: string | null +}; + +type TreatmentsWithConfig = { + [featureName: string]: TreatmentWithConfig +}; +``` + +As you can see from the object structure, the config will be a stringified version of the configuration JSON defined in the Split user interface. If there is no configuration defined for a treatment, the SDK returns `null` for the config parameter. + +In some instances, you may want to evaluate treatments for multiple feature flags at once using flag sets. In that case, you can use the `useSplitTreatments` hook with the `flagSets` property rather than the `names` property. Like `names`, the `flagSets` property is an array of strings, each one corresponding to a different flag set name. + + + +```javascript +import { useSplitTreatments } from '@splitsoftware/splitio-react'; +import MyComponentV1 from './MyComponentV1'; +import MyComponentV2 from './MyComponentV2'; + +const flagSets = ['frontend', 'client_side']; +const featureName = 'FEATURE_FLAG_NAME'; + +function renderContent(treatmentWithConfig, props) { + const { treatment, config } = treatmentWithConfig; + + if (treatment === 'on') return (); + + return (); +} + +function MyComponentToggle (props) { + + const { treatments, isReady } = useSplitTreatments({ flagSets: flagSets }); + + return isReady ? + // If your flag sets are not properly configured, `treatments` might be an empty object + // or it might not contain the feature flags you expect. + renderContent(treatments[featureName] || { treatment: 'control' }, props) : + ; +} + +export default MyComponentToggle; +``` + + +```javascript +import { SplitTreatments } from '@splitsoftware/splitio-react'; +import MyComponentV1 from './MyComponentV1'; +import MyComponentV2 from './MyComponentV2'; + +const flagSets = ['frontend', 'client_side']; +const featureName = 'FEATURE_FLAG_NAME'; + +export default class MyComponentToggle extends React.Component { + + renderContent(treatmentWithConfig) { + const { treatment, config } = treatmentWithConfig; + + if (treatment === 'on') return (); + + return (); + } + + render() { + return ( + + {({ treatments, isReady }) => { + return isReady ? + // If your flag sets are not properly configured, `treatments` might be an empty object + // or it might not contain the feature flags you expect. + this.renderContent(treatments[featureName] || { treatment: 'control' }) : + ; + }} + + ); + } +} +``` + + + +### Attribute syntax + +To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK needs to be passed an attribute map at runtime. In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the underlying `getTreatmentsWithConfig` or `getTreatmentsWithConfigByFlagSets` call, whether you are evaluating using the `names` or `flagSets` property respectively. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in the Split user interface to decide whether to show the `on` or `off` treatment to this account. The SDK supports five types of attributes: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: + +* **Strings:** Use type String. +* **Numbers:** Use type Number. +* **Dates: ** Use type Date and express the value in `milliseconds since epoch`.
*Note:* Milliseconds since epoch is expressed in UTC. If your date or date-time combination is in a different timezone, first convert it to UTC, then transform it to milliseconds since epoch. +* **Booleans:** Use type Boolean. +* **Sets:** Use type Array. + + + + +```javascript +const attributes = { + // date attributes are handled as `millis since epoch` + registered_date: new Date('YYYY-MM-DDTHH:mm:ss.sssZ').getTime(), + // this string will be compared against a list called `plan_type` + plan_type: 'growth', + // this number will be compared agains a const value called `deal_size` + deal_size: 10000, + // this boolean will be compared against a const value called `paying_customer` + paying_customer: true, + // this array will be compared against a set called `permissions` + permissions: ['read', 'write'] +}; + +const ComponentWithTreatments = () => { + const { treatments, isReady } = useSplitTreatments({ names: [featureName], attributes: attributes }); + + const treatment = treatments[featureName].treatment; + + return isReady ? + treatment === 'on' ? + : + : + +}; +``` + + +```javascript +const attributes: SplitIO.Attributes = { + // date attributes are handled as `millis since epoch` + registered_date: new Date('YYYY-MM-DDTHH:mm:ss.sssZ').getTime(), + // this string will be compared against a list called `plan_type` + plan_type: 'growth', + // this number will be compared agains a const value called `deal_size` + deal_size: 10000, + // this boolean will be compared against a const value called `paying_customer` + paying_customer: true, + // this array will be compared against a set called `permissions` + permissions: ['read', 'write'] +}; + +const ComponentWithTreatments = () => { + const { treatments, isReady } = useSplitTreatments({ names: [featureName], attributes: attributes }); + + const treatment: SplitIO.Treatment = treatments[featureName].treatment; + + return isReady ? + treatment === 'on' ? + : + : + +}; +``` + + + +### Binding attributes to the client + +Attributes can optionally be bound to the `useSplitClient` hook or the `SplitFactoryProvider` component via props. These attributes are stored in memory and used in every evaluation to avoid the need to keep the attribute set accessible through the whole app. When an evaluation is called, for example, with the `useSplitTreatments` hook, the attributes provided (if any) at evaluation time are combined with the ones that are already loaded into the SDK memory, with the ones provided at evaluation time taking precedence. This enables those attributes to be overridden or hidden for specific evaluations. + +An attribute is considered valid if it follows one of the types listed below: +- String +- Number +- Boolean +- Array + +The SDK validates these before storing them and if there are invalid or missing values, possibly indicating an issue, the storing is not performed. + +To use these methods, refer to the example below: + + + +```javascript +import { SplitFactoryProvider, useSplitClient, useSplitTreatments } from '@splitsoftware/splitio-react'; + +// Assuming this is how the user setup the factory: +const sdkConfig = { + core: { + authorizationKey: 'YOUR_CLIENT_SIDE_API_KEY', + key: 'nicolas@user' + } +}; +// These are the attributes bound to the main client, for key 'nicolas@user' +const userAttributes = { + permissions: ["read", "write"] +}; +// These are the attributes bound to a secondary client, for key 'nicolas@account' +const accountAttributes = { + permissions: "read" +}; +// The are additional attributes for the evaluation +const treatmentAttributes = { + deal_size: 10000 +}; + +const App = () => ( + + + +); + +function MyComponent(props) { + + /* + * Using the main client bound to the key passed in the config for evaluations + * and the attributes received on SplitFactoryProvider attributes prop. + */ + const { isReady, treatments } = useSplitTreatments({ names: ['USER_FEATURE_FLAG_NAME'], attributes: treatmentAttributes }); + // Do something with the treatments for the 'nicolas@user' key. + // This evaluation combines SplitFactoryProvider component attributes with the ones provided to the useSplitTreatments hook. + // Then it evaluates with attributes = { permissions: ["read", "write"], deal_size: 10000 } + + // You can retrieve clients for as many keys as you need, although it's not recommended to create more than you absolutely need to. + // Keep in mind that if it is the first time you retrieve a client, it might not be ready yet. + const { client: accountClient, isReady } = useSplitClient({ splitKey: this.props.accountId, attributes: accountAttributes }); + + if (isReady) { + // `accountClient` is ready to evaluate treatments as usual. To see more information about the client API, refer to the docs for JavaScript SDK. + // In this evaluation, `treatmentAttributes`` are combined with `accountAttributes` + const accountTreatments = accountClient.getTreatments(['ACCOUNT_FEATURE_FLAG_NAME', 'ACCOUNT_FEATURE_FLAG_NAME_2'], treatmentAttributes); + + // Do something with the treatments for the accountId key. + // This evaluation is for another client, so factory attributes stored in main client will not be taking part. + // This evaluation combines `treatmentAttributes` with `accountAttributes`. + // The evaluation attributes are { permissions: "read", deal_size: 10000 } + return (
{...}
); + } else { + // `accountClient` is not ready + return (
{...}
); + } + +} +```
+ +```javascript +import { SplitFactoryProvider, SplitClient, SplitTreatments } from '@splitsoftware/splitio-react'; + +// Assuming this is how the user setup the factory: +const sdkConfig = { + core: { + authorizationKey: 'YOUR_CLIENT_SIDE_API_KEY', + key: 'nicolas@user' + } +}; +// These are the attributes bound to the main client, for key 'nicolas@user' +const userAttributes = { + permissions: ["read", "write"] +}; +// These are the attributes bound to a secondary client, for key 'nicolas@account' +const accountAttributes = { + permissions: "read" +}; +// The are additional attributes for the evaluation +const treatmentAttributes = { + deal_size: 10000 +}; + +const App = () => ( + + + +); + +class MyComponent extends React.Component { + + render() { + return ( +
+ { + /* + * Using SplitTreatments as a descendant of the SplitFactoryProvider with no SplitClient wrapping it, uses the main client + * bound to the key passed in the config for evaluations and the attributes received on SplitFactoryProvider attributes prop. + */ + } + + {({ isReady, treatments }) => { + // Do something with the treatments for the 'nicolas@user' key. + // This evaluation combines SplitFactoryProvider component attributes with the ones on the SplitTreatments component. + // Then it evaluates with attributes = { permissions: ["read", "write"], deal_size: 10000 } + }} + + { + /* + * To use another client, use the SplitClient component. Keep in mind that + * this client might not be ready yet if it's just being created and still downloading segments + * for the new key. Use the `isReady` property to check that. + */ + } + + + {({ isReady, treatments }) => { + // Do something with the treatments for the accountId key. + // This evaluation is for another client, so attributes stored in main client will not be taking part. + // This evaluation combines `treatmentAttributes` with `accountAttributes`. + // The evaluation attributes are { permissions: "read", deal_size: 10000 } + }} + + +
+ ); + } +} +``` +
+
+ +### Shutdown + +If the `SplitFactoryProvider` component is created with a `config` prop, then the component automatically instantiates and shuts down the SDK factory when the component is mounted and unmounted respectively. Otherwise, if the component is created by passing an SDK factory via the `factory` prop, you should handle the [shutdown](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#shutdown) yourself. + +## Track + +Use the `client.track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Tracking events through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your features on your users' actions and metrics. + +[Learn more](https://help.split.io/hc/en-us/articles/360020585772) about using track events in feature flags. + +To track events, you must follow two steps: +1. Retrieve the `client.track` method, which is available through the `useTrack` hook or the `useSplitClient` hook. +2. Execute the `track` method call, passing in the traffic type and event info as arguments. + +In the examples below, you can see that tracking events can take up to four arguments. The proper data type and syntax for each are: + +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: + * Contains 63 characters or fewer. + * Starts with a letter or number. + * Contains only letters, numbers, hyphen, underscore, or period. + * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` +* **VALUE:** (Optional) The value to be used in creating the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is **Integer** or **Float**. +* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. + +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event to be sent back to Split's servers on the next event post. The SDK returns `false` if the current queue size is equal to the config set by `eventsQueueSize` config or if an incorrect input has been provided. + +In case a bad input is provided, you can read more about our [SDK's expected behavior](https://help.split.io/hc/en-us/articles/360020585772-Track-events). + +Remember that: +- You must follow [React Hook rules](https://react.dev/reference/rules/rules-of-hooks) when using the `useTrack` or `useSplitClient` hooks, i.e., they must be invoked at the top level of your component or in custom hooks. +- The `client.track` method doesn't require the client to be ready, but it implies a side effect. Therefore, it should be invoked [outside the component render phase](https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render), such as in a `useEffect` hook or an event handler. + + + +```javascript +import { useTrack } from '@splitsoftware/splitio-react'; + +function MyComponent() { + // With the useTrack hook you can get access to the regular .track() method of the default client. + const track = useTrack(); + + // If you need to track events for a different key or just want to make sure you're tracking for the right key. + const otherTrack = useTrack('key'); + + // Here's what the track function signature looks like: + // track(trafficType: string, eventType: string, value?: number, properties?: Properties): boolean; + + // Track events into an effect or an event handler to make sure they're not tracked on every render. + useEffect(() => { + // Now you can track your events by passing at least the traffic type and the event type. + let queued = track('user', 'logged_in'); + + // Example with both a value and properties + const properties = { package : "premium", admin : true, discount : 50 }; + queued = track('user', 'page_load_time', 83.334, properties); + + // If the event doesn't have a value but do have properties, skip the value by passing it null. + queued = track('user', 'logged_in', null, properties); + }, []); + + return +} +``` + + +```javascript +import { useTrack } from '@splitsoftware/splitio-react'; + +function MyComponent() { + // With the useSplitClient hook you can get access to the default client and its .track() method + const defaultClient = useSplitClient().client; + + // If you need to track events for a different key or just want to make sure you're tracking for the right key + const otherClient = useSplitClient({ splitKey: 'key' }); + + // Track events into an effect or an event handler to make sure they're not tracked on every render. + useEffect(() => { + // Now you can track your events by passing at least the traffic type and the event type. + let queued: boolean = client.track('user', 'logged_in'); + + // Example with both a value and properties + const properties: SplitIO.Properties = { package : "premium", admin : true, discount : 50 }; + queued = client.track('user', 'page_load_time', 83.334, properties); + + // If the event doesn't have a value but does have properties, skip the value by passing it null. + queued = client.track('user', 'logged_in', null, properties); + }, []); + + return +} +``` + + + +## Configuration + +The SDK has a number of knobs for configuring performance. Each knob is tuned to a reasonable default. However, you can override the value while providing the config to the `SplitFactoryProvider` as shown in the Initialization section of this doc. To learn about the available configuration options, go to the [JavaScript SDK Configuration section](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#configuration). + +## Localhost mode + +For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To do this, start the Split SDK in **localhost** mode (also called off-the-grid mode). In this mode, the SDK neither polls or updates from Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the features. + +When instantiating the SDK in localhost mode, your `authorizationKey` is `"localhost"`. Define the feature flags you want to use in the `features` object map. All `useSplitTreatments` calls for a feature flag return the treatment (and config, if defined) that you have defined in the map. You can then change the treatment as necessary for your testing. If you want to update a treatment or a config, or to add or remove feature flags from the mock cache, update the properties of the `features` object you've provided. The SDK simulates polling for changes and updates from the `features` object. Do not assign a new object to the `features` property because the SDK has a reference to the original object and will not detect the change. + +Any feature that is not provided in the `features` map returns the [control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment) if the SDK is asked to evaluate them. Use the following additional configuration parameters when instantiating the SDK in `localhost` mode: + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| scheduler.offlineRefreshRate | The refresh interval for the mocked features treatments. | 15 | +| features | A fixed mapping of which treatment to show for our mocked features. | {}
By default we have no mocked features. | + +To use the SDK in localhost mode, replace the SDK Key with "localhost", as shown in the following test examples. Note that you can define the object between a feature flag name and treatment directly or use a map to define both a treatment and a dynamic configuration. + +If you define just a string as the value for a feature flag name, the config returned by the SDK is null. If you use a map, we return the specified treatment and the specified config, which can also be null. + + + +```javascript +import React from "react"; +// React testing library: https://www.npmjs.com/package/@testing-library/react +import { render, waitFor } from "@testing-library/react"; +import { SplitFactoryProvider, useSplitTreatments } from "@splitsoftware/splitio-react"; + +const config = { + core: { + authorizationKey: 'localhost', + key: 'user_id' + }, + features: { + 'reporting_v2': 'on', // example with just a string value for the treatment + 'billing_updates': { treatment: 'visa', config: '{ "color": "blue" }' }, // example of a defined config + 'show_status_bar': { treatment: 'off', config: null } // example of a null config + } +}; + +const MyComponent = () => { + const { isReady, treatments } = useSplitTreatments({ names=['reporting_v2'] }); + + return
{`${isReady ? 'SDK ready.' : 'SDK not ready.'} Feature flag reporting_v2 is ${treatments['reporting_v2'].treatment}`}
; +} + +const MyApp = () => { + return ( + + + + ); +}; + +describe('MyApp', () => { + test('renders the correct treatment', async () => { + const { getByText, findByText } = render(); + + // On the initial rendered output, the SDK is not ready. So treatment values are control. + expect(getByText('SDK not ready. Feature flag reporting_v2 is control')).toBeTruthy(); + + // In localhost mode, the SDK is ready and the component re-rendered with the mocked treatment on next event-loop tick. + // So we use `findByText` to wait for the component to update. + expect(await findByText('SDK ready. Feature flag reporting_v2 is on')).toBeTruthy(); + + // `waitFor` (https://testing-library.com/docs/dom-testing-library/api-async/#waitfor) can also be used: + expect(await waitFor(() => getByText('SDK ready. Feature flag reporting_v2 is on'))); + }); +}); +``` +
+ +```javascript +import React from 'react'; +// Enzyme testing utility: https://www.npmjs.com/package/enzyme +import { mount } from 'enzyme'; +import { SplitFactoryProvider, useSplitTreatments } from '@splitsoftware/splitio-react'; + +const config = { + core: { + authorizationKey: 'localhost', + key: 'user_id' + }, + features: { + 'reporting_v2': 'on', // example with just a string value for the treatment + 'billing_updates': { treatment: 'visa', config: '{ "color": "blue" }' }, // example of a defined config + 'show_status_bar': { treatment: 'off', config: null } // example of a null config + } +}; + +const MyComponent = () => { + const { isReady, treatments } = useSplitTreatments({ names=['reporting_v2'] }); + + return
{`${isReady ? 'SDK ready.' : 'SDK not ready.'} Feature flag reporting_v2 is ${treatments['reporting_v2'].treatment}`}
; +} + +const MyApp = () => { + return ( + + + + ); +}; + +describe('MyApp', () => { + test('renders the correct treatment', (done) => { + const wrapper = mount(); + + // On the initial rendered output, the SDK is not ready. So treatment values are control. + expect(wrapper.html().includes('SDK not ready. Feature flag reporting_v2 is control')).toBeTruthy(); + + // In localhost mode, the SDK is ready and the component re-rendered with the mocked treatment on next event-loop tick. + // So we call `wrapper.update()` in a timeout callback to re-render the component. + setTimeout(() => { + wrapper.update(); + expect(wrapper.html().includes('SDK ready. Feature flag reporting_v2 is on')).toBeTruthy(); + + done(); + }, 0); + }); +}); +``` +
+ +```javascript +import React from 'react'; +// React Test Renderer: https://reactjs.org/docs/test-renderer.html +import { create } from 'react-test-renderer'; +import { SplitFactoryProvider, useSplitTreatments } from '@splitsoftware/splitio-react'; + +const config = { + core: { + authorizationKey: 'localhost', + key: 'user_id' + }, + features: { + 'reporting_v2': 'on', // example with just a string value for the treatment + 'billing_updates': { treatment: 'visa', config: '{ "color": "blue" }' }, // example of a defined config + 'show_status_bar': { treatment: 'off', config: null } // example of a null config + } +}; + +const MyComponent = () => { + const { isReady, treatments } = useSplitTreatments({ names=['reporting_v2'] }); + + return
{`${isReady ? 'SDK ready.' : 'SDK not ready.'} Feature flag reporting_v2 is ${treatments['reporting_v2'].treatment}`}
; +} + +const MyApp = () => { + return ( + + + + ); +}; + +describe('MyApp', () => { + test('renders the correct treatment', (done) => { + const root = create(); + + // On the initial rendered output, the SDK is not ready. So treatment values are control. + expect(root.toJSON().children[0]).toEqual('SDK not ready. Feature flag reporting_v2 is control'); + + // In localhost mode, the SDK is ready and the component re-rendered with the mocked treatment on next event-loop tick. + // So we call `root.update` in a timeout callback to re-render the component. + setTimeout(() => { + root.update(); + expect(root.toJSON().children[0]).toEqual('SDK ready. Feature flag reporting_v2 is on'); + + done(); + }, 0); + }); +}); +``` +
+
+ +## Manager + +Use the Split manager to get a list of features available to the Split client. To access the Manager in your code base, use the `useSplitManager` hook. + + + + +```javascript +import { useSplitManager } from '@splitsoftware/splitio-react'; + +const { manager, isReady } = useSplitManager(); + +// Make sure the SDK is ready to return Split's data from the manager object +if (isReady) { + // Manager is ready to be used. + const flagNames = manager.names(); +} +``` + + +```javascript +import { useSplitManager } from '@splitsoftware/splitio-react'; + +const { manager, isReady } = useSplitManager(); + +// Make sure the SDK is ready to return Split's data from the manager object +if (isReady) { + // Manager is ready to be used. + const flagNames: SplitIO.SplitNames = manager.names(); +} +``` + + + +To find all the details on the Manager available methods, see the [JavaScript SDK Manager section.](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#manager) + +## Listener + +Split SDKs send impression data back to Split servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression listener*. For that purpose, the SDK's configurations have a parameter called `impressionListener` where an implementation of `ImpressionListener` could be added. This implementation **must** define the `logImpression` method and it receives data in the following schema. + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| impression | | Object | Impression object that has the feature, key, treatment, label, etc. | +| attributes | Object | A map of attributes used on the evaluation (if any). | +| sdkLanguageVersion | String| The version of the SDK. In this case the language is `react` plus the version of the underlying SDK. | + +:::info[Note] +There are two additional keys on this object, `ip` and `hostname`. They are not used on the browser. +::: + +## Implement custom impression listener + +The following is an example of how to implement a custom impression listener: + + + + +```javascript +import { SplitFactoryProvider } from '@splitsoftware/splitio-react'; + +function logImpression(impressionData) { + // do something with the impression data. +} + +// Create the config for the SDK factory. +const sdkConfig = { + core: { + authorizationKey: 'YOUR_CLIENT_SIDE_SDK_KEY', + key: 'key' + }, + impressionListener: { + logImpression: logImpression + } +}); + +// Using the SplitFactoryProvider component. +const App = () => ( + + + +); +``` + + +```javascript +import { SplitFactoryProvider } from '@splitsoftware/splitio-react'; + +class MyImprListener implements SplitIO.IImpressionListener { + logImpression(impressionData: SplitIO.ImpressionData) { + // do something with impressionData + } +} + +// Create the config for the SDK factory. +const sdkConfig = { + core: { + authorizationKey: 'YOUR_CLIENT_SIDE_SDK_KEY', + key: 'key' + }, + impressionListener: { + logImpression: new MyImprListener() + } +}); + +// Using the SplitFactoryProvider component. +const App: React.ComponentType = () => ( + + + +); +``` + + + +An impression listener is called asynchronously from the corresponding evaluation, but is almost immediate. + +Even though the SDK does not fail if there is an exception in the listener, do not block the call stack. + +## Logging + +To enable SDK logging in the browser, see how the [SDK Logging](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#logging) works. + +This library own logger is not configurable yet, but will be very soon! + +## Advanced use cases + +This section describes advanced use cases and features provided by the SDK. + +### Instantiate multiple SDK clients + +Each JavaScript SDK client is tied to one specific customer id at a time which usually belongs to one traffic type (for example, `user`, `account`, `organization`). This enhances performance and reduces data cached within the SDK. + +Split supports the ability to release based on multiple traffic types. With traffic types, you can release to `users` in one feature flag and `accounts` in another. If you are unfamiliar with using multiple traffic types, you can learn more [here](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type). + +If you need to roll out feature flags by different traffic types, instantiate multiple SDK clients, one for each traffic type. For example, you may want to roll out the feature `user-poll` by `users` and the feature `account-permissioning` by `accounts`. You can do this by retrieving different clients using the `useSplitClient` hook. + +See some examples below: + + + +```javascript +import { SplitFactoryProvider, useSplitClient } from '@splitsoftware/splitio-react'; + +// Assuming this is how we setup the factory: +const sdkConfig = { + core: { + authorizationKey: 'YOUR_CLIENT_SIDE_SDK_KEY', + key: 'nicolas@split' + } +}; + +const App = () => ( + + + +); + +// An example of how you can access a different SDK client to track events or evaluate flags in a function component with `useSplitClient` hook. +function MyComponentWithFlags(props) { + + // Calling useSplitClient with no `splitKey` parameter returns the client on the Split context. + // In this case the main client associated with the key 'nicolas@split' provided on initialization. + const { client: inContextClient } = useSplitClient(); + + // If client was already instantiated, it is just a getter like in the regular JS SDK. + const { client: myUserClient } = useSplitClient({ splitKey: 'nicolas@split' }); + + console.log(inContextClient === myUserClient); // logs "true" + + // You can retrieve clients for as many keys as you need, although not recommended to create more than you absolutely need to. + // Keep in mind that if it is the first time you retrieve a client, it might not be ready yet. + const { client: accountClient, isReady: isReadyAccountClient } = useSplitClient({ splitKey: this.props.accountId }); + + if (isReadyAccountClient) { + // `accountClient` is ready to evaluate treatments as usual. To see more information about the client API go to the docs for JavaScript SDK. + const accountTreatments = accountClient.getTreatments(['ACCOUNT_FEATURE_FLAG_NAME', 'ACCOUNT_FEATURE_FLAG_NAME_2']); + + return (
{...}
); + } else { + // `accountClient` is not ready + return (
{...}
); + } + +} +```
+ +```javascript +import { SplitFactoryProvider, SplitClient } from '@splitsoftware/splitio-react'; + +// Assuming this is how we setup the factory: +const sdkConfig = { + core: { + authorizationKey: 'YOUR_CLIENT_SIDE_SDK_KEY', + key: 'nicolas@split' + } +}; + +const App = () => ( + + + +); + +class MyComponentWithFlags extends React.Component { + + // See how you can use the component to evaluate for multiple clients in your templates. + render() { + return ( +
+ { + /* + * Using SplitTreatments as a descendant of the SplitFactoryProvider with no SplitClient wrapping it, + * uses the main client bound to the key passed in the config for evaluations. + * In this case the used key for evaluations is 'nicolas@split'. + */ + } + + {({ isReady, treatments }) => { + // Do something with the treatments for the user traffic type. + }} + + { + /* + * To use another client, you can use the SplitClient component. Keep in mind that + * this client might not be ready yet if it's just being created and still downloading segments + * for the new key, but you can use the isReady property to block until ready. + */ + } + + + {({ isReady, treatments }) => { + // Do something with the treatments for the account traffic type. + }} + + +
+ ); + } +} +``` +
+
+ +:::info[Number of SDK instances] +While the SDK does not put any limitations on the number of instances that can be created, we strongly recommend keeping the number of SDKs down to **one** or **two**. +::: + +### Subscribe to events + +The underlying JavaScript SDK has four different events: + +* `SDK_READY_FROM_CACHE`. This event fires once the SDK is ready to evaluate treatments using a version of your rollout plan cached in localStorage from a previous session (which might be stale). If there is data in localStorage, this event fires almost immediately, since access to localStorage is fast; otherwise, it doesn't fire. +* `SDK_READY`. This event fires once the SDK is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Split servers. +* `SDK_READY_TIMED_OUT`. This event fires if there is no cached version of your rollout plan cached in localStorage, and the SDK could not download the data from Split servers within the time specified by the `readyTimeout` configuration parameter. This event does not indicate that the SDK initialization was interrupted. The SDK continues downloading the rollout plan and fires the `SDK_READY` event when finished. This delayed `SDK_READY` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. +* `SDK_UPDATE`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in the Split user interface. + +While you could potentially access the JavaScript SDK client from the Split context and track this yourself, this is not trivial. The `useSplitClient` and `useSplitTreatments` hooks accept four optional boolean parameters to trigger the re-render of the component. If the parameters are set to `true` (which is the default), the component re-renders when an `SDK_READY`, `SDK_READY_FROM_CACHE`, `SDK_UPDATE` or `SDK_READY_TIMED_OUT` event fires, and you can take action if you desire to do so. + +* For `SDK_READY`, you can set the `updateOnSdkReady` parameter. +* For `SDK_READY_FROM_CACHE`, you can set the `updateOnSdkReadyFromCache` parameter. +* For `SDK_READY_TIMED_OUT`, you can set the `updateOnSdkTimedout` parameter. +* For `SDK_UPDATE`, you can set the `updateOnSdkUpdate` parameter. +The default value for all these parameters is `true`. + +The `useSplitClient` and `useSplitTreatments` hooks return the SDK client and treatment evaluations respectively, together with a set of **status properties** to conditionally render the component. + +These properties consist of the following: + +- `isReady`: a boolean indicating if the `SDK_READY` event was triggered. +- `isReadyFromCache`: a boolean indicating if the `SDK_READY_FROM_CACHE` event was triggered. +- `hasTimedout`: a boolean indicating if the `SDK_READY_TIMED_OUT` event was triggered. +- `isTimedout`: a boolean indicating if the `SDK_READY_TIMED_OUT` event was triggered and the SDK is not ready to be consumed. Formally, `isTimedout` is equivalent to `hasTimeout && !isReady`. +- `lastUpdate`: timestamp of the last listened event. + +Find an example below: + + + +```javascript +function MyApp() { + // Evaluates feature flags for the main client bound to the key passed in the factory config. + const { treatments, isReady, isReadyFromCache, hasTimedout, lastUpdate } = useSplitTreatments({ names: ['USER_FEATURE_FLAG_NAME'] }); + + // But we can evaluate at a per client basis, and choose when to trigger a re-render. + // For example, the accountId we only want to update on SDK_READY and SDK_READY_TIMED_OUT. Not on SDK_READY_FROM_CACHE or SDK_UPDATE. + const { treatments: accountTreatments, isReady: isReadyAccount, hasTimedout: hasTimedoutAccount } = useSplitTreatments({ + names: ['ACCOUNT_FEATURE_FLAG_NAME', 'ACCOUNT_FEATURE_FLAG_NAME_2'], + splitKey: accountId, + updateOnSdkReadyFromCache: false, // true by default + updateOnSdkUpdate: false // true by default + }); + + return ( +
+ { + // Do something with the treatments for the main key + } + { + // Do something with the treatments for the account key + } +
+ ); +}; + +const App = () => ( + + + +); +``` +
+ +```javascript +function MyApp({ isReady, isReadyFromCache, isTimedout, hasTimedout, lastUpdate, factory, client }) { + return ( +
+ + { + // Do something with the treatments for the user key defined in the config. + } + + + { /* But we can override that setup at a per client basis, so the account one we only want to + * update on SDK_READY and SDK_READY_TIMED_OUT */ } + + + {({ isReady, isReadyFromCache, isTimedout, hasTimedout, lastUpdate, treatments }) => { + // Do something with the treatments for the `accountId` key. + }} + + +
+ ); +}; + +const App = () => ( + /* Here MyApp function component is passed as a render prop component and not as a React element like . + * MyApp is called with the SplitContext object as param, which contains the SDK factory, client and status properties. + * As a child of the SplitClient component, it is called on SDK_READY, SDK_READY_FROM_CACHE, + * and SDK_UPDATE events, but not on SDK_READY_TIMED_OUT since the `updateOnSdkTimedout` prop is false. + */ + + + {MyApp} + + +); +``` +
+
+ +You can also access the `SplitContext` directly in your components for checking the readiness state of the client. Via the React [`useContext`](https://react.dev/reference/react/useContext) function, you can access the value of the `SplitContext` as shown below: + + + + +```javascript +import { useContext } from 'react'; +import { SplitContext } from "@splitsoftware/splitio-react"; + +const MyComponent = () => { + const { isReady, isTimedout } = useContext(SplitContext); + return isReady || isTimedout ? + : + +} +``` + + +```typescript +import { useContext } from 'react'; +import { SplitContext, ISplitContextValues } from "@splitsoftware/splitio-react"; + +const MyComponent: React.ComponentType = () => { + const { isReady, isTimedout }: ISplitContextValues = useContext(SplitContext); + return isReady || isTimedout ? + : + +} +``` + + + +The `SplitContext` value object has the following structure: + + + +```typescript +interface SplitContextValue { + factory: SplitIO.IBrowserSDK, + client: SplitIO.IBrowserClient, + isReady: boolean, + isReadyFromCache: boolean, + hasTimeout: boolean, + isTimedout: boolean, + isDestroyed: boolean, + lastUpdate: number, +} +``` + + + +The `SplitContext` exposes the internal factory and client instances of JavaScript SDK which is used underneath. While the React SDK should enable most use cases when using React, you might be in a situation where you must use the SDK factory functionality directly, for example, to manage [User Consent](#user-consent) and [Logging](#logging) configurations. We discourage direct use of these instances unless necessary. + +### User consent + +The SDK factory allows you to disable the tracking of events and impressions until user consent is explicitly granted or declined. To learn how to configure this feature, refer to the [JavaScript SDK User consent section.](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#user-consent) + +### Server-Side Rendering + +The SDK follows the rules of the React component lifecycle by correctly handling the SDK factory creation and destroy side-effects and supporting running its components on the server side. + +When running server side, the `config` prop of the `SplitFactoryProvider` must be used, and the `factory` prop must remain unassigned. This way, the HTML rendered on the server side will match the first render on the client side, because both sides will conditionally render your components with "no ready" SDK instances. At this point, status properties like `isReady` are `false` and the treatments retrieved using `useSplitTreatments` are `'control'`. Following this state, the SDK factory is initialized on the `SplitFactoryProvider` effect. Thus, the SDK factory will be ready on the client side on a subsequent render, but will not be initialized on the server side. + +Usage for SSR is shown in the following code examples: + + + +```javascript +// App.jsx +const myConfig = { ... }; // SDK configuration object + +export const App = () => { + return ( + + + + ) +}; + +// server.jsx +import { renderToString } from 'react-dom/server'; +import { App } from './App'; + +app.use('/', (request, response) => { + const reactHtml = renderToString(); + response.send(` + + + My App +
${reactHtml}
+ + + `); +}); + +// client.jsx, bundled into client.js +import { hydrateRoot } from 'react-dom/client'; +import { App } from './App' + +const domNode = document.getElementById('root'); +const root = hydrateRoot(domNode, ); +``` +
+ +```javascript +// pages/index.jsx +export const getServerSideProps = (async () => { + ... + const myConfig = { ... } // SDK configuration object + return { props: { myConfig, ... } } +}); + +export default function Page(props) { + return ( + + + + ); +}; +``` + + +```javascript +// SDK components are traditional "Client" components, so you need to use the 'use client' directive to nest with Server components +// https://nextjs.org/docs/app/building-your-application/rendering/client-components + +// app/providers.jsx +'use client' + +import { SplitFactoryProvider } from '@splitsoftware/splitio-react'; + +export default function Providers({ children }) { + const [myConfig] = React.useState(() => { + return { ... } // SDK configuration object + }); + + return ( + {children} + ); +}; + +// app/layout.jsx +import Providers from './providers'; + +export default function RootLayout({ children }) { + return ( + + + + {children} + + + ); +}; + +// app/page.jsx +import { MyComponentWithFeatureFlags } from './MyComponentWithFeatureFlags'; + +export default async function Home() { + const props = await getAsyncData(); + + return ( + + ); +} + +// app/MyComponentWithFeatureFlags.jsx +'use client' + +import { useSplitTreatments } from '@splitsoftware/splitio-react'; + +const FEATURE_FLAG_NAME = 'test_split'; + +export const MyComponentWithFeatureFlags = (props) => { + const { treatments, isReady } = useSplitTreatments({ names: [FEATURE_FLAG_NAME] }); + + return isReady ? + treatments[FEATURE_FLAG_NAME].treatment === 'on' ? + : + : + +}; +``` + +
+ +### Usage with React Native SDK + +The Split React SDK can be used in React Native applications by combining it with the React Native SDK. For that, you need to instantiate a factory with the React Native SDK and pass it to the React SDK `SplitFactoryProvider` component. The React SDK will use the factory to create the client and evaluate the feature flags. + + + +```javascript +import { SplitFactory } from '@splitsoftware/splitio-react-native'; +import { SplitFactoryProvider } from '@splitsoftware/splitio-react'; + +const reactNativeFactory = SplitFactory({ + core: { + authorizationKey: 'YOUR_CLIENT_SIDE_SDK_KEY', + key: 'key' + }, + ... +}); + +// Using the SplitFactoryProvider component, MyApp component and all it's descendants will have access to the SDK functionality using the provided React Native SDK factory. +const App = () => ( + + + +); +``` + + + +## Example apps + +The following are example applications showing how you can integrate the React SDK into your code. + +* [React](https://github.com/splitio/react-sdk-examples) +* [React with TypeScript](https://github.com/splitio/react-typescript-sdk-examples) diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/redux-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/redux-sdk.md new file mode 100644 index 00000000000..d32088f01b5 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/redux-sdk.md @@ -0,0 +1,1166 @@ +--- +title: Redux SDK +sidebar_label: Redux SDK +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +This guide provides detailed information about our Redux SDK. This library is built on top of our regular [JavaScript SDK](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK) to ease the integration in applications using Redux by providing a [reducer](https://redux.js.org/tutorials/essentials/part-1-overview-concepts#reducers) to manage the Split-related state, a set of [actions](https://redux.js.org/tutorials/essentials/part-1-overview-concepts#actions) that you can use to interact with the SDK, [selectors](https://redux.js.org/tutorials/essentials/part-1-overview-concepts#selectors) to easily get Split-desired data, and helper functions to access some of the underlying SDK functionality to support all use cases. + +Taking advantage of our JavaScript SDK being isomorphic, we also support [SSR](https://redux.js.org/usage/server-rendering) by using the underlying [SDK in Node.js](https://help.split.io/hc/en-us/articles/360020564931-Node-js-SDK) + +This library also offers some extra features for users of React that are using [react-redux](https://github.com/reduxjs/react-redux) bindings. + +All of our SDKs are open source. Go to our [Redux SDK GitHub repository](https://github.com/splitio/redux-client) to see the source code. + +:::info[Migrating from v1.x to v2.x] +Refer to the [migration guide](https://github.com/splitio/redux-client/blob/development/MIGRATION-GUIDE.md) for information on upgrading to v2.x. +::: + +## Language support and requirements + +This SDK is compatible with Redux v3 and later. It requires the [redux-thunk](https://github.com/reduxjs/redux-thunk) package to be installed on the app, which is included by default if your project is using the [Redux Toolkit](https://redux-toolkit.js.org/). This means you don't need to run `npm install redux-thunk` if Redux Toolkit is already installed. + +For `react-redux` users, the SDK supports its v4 and later. + +In SSR setups, our library code is prepared to run in Node.js 14+. + +## Initialization + +Set up Split in your code base in two steps. + +### 1. Import the SDK into your project + +The SDK is published using `npm`, so it's fully integrated with your workflow. You should be able to add it with `yarn` too. + + + +```bash +npm install --save @splitsoftware/splitio-redux +``` + + +```bash +yarn add @splitsoftware/splitio-redux +``` + + + +### 2. Integrate the SDK in your application + +You need to combine the Split reducer with yours when creating your store and use the `initSplitSdk` action creator, which returns a thunk, to set things in motion. You can use the [combineReducers](https://redux.js.org/api/combinereducers#combinereducersreducers) function of Redux on the `splitio` key. You can mount it at a different key but might require some extra code if you use the specific functionality for [react-redux](#advanced-usage-with-react--redux). + +For the client side, the Redux documentation [recommends](https://redux.js.org/introduction/getting-started#basic-example) creating a single store to be used as the source of truth for your state. This is where we'll plug in the Split reducer. + +For Server Side Rendering, the Redux documentation [suggests](https://redux.js.org/usage/server-rendering#handling-the-request) creating a store per request, which is why we provide a function to create stores, where each instance will include the Split reducer. + + + +```javascript +import { configureStore, combineReducers } from '@reduxjs/toolkit'; +import { splitReducer, initSplitSdk } from '@splitsoftware/splitio-redux'; + +const sdkBrowserConfig = { + core: { + authorizationKey: 'YOUR_SDK_KEY', + // key represents your internal user id, or the account id that + // the user belongs to. + // This could also be a cookie you generate for anonymous users. + key: 'key' + } +}; + +// Create the Redux Store +const store = configureStore( + combineReducers({ + splitio: splitReducer, + ... // Combine Split reducer with your own reducers + }), + // Split SDK requires thunk middleware, which is included by default by Redux Toolkit +); + +// Initialize the SDK by calling the initSplitSdk and passing the config in the parameters object. +store.dispatch(initSplitSdk({ config: sdkBrowserConfig })); + +export default store; +``` + + +```javascript +import { createStore, applyMiddleware, combineReducers } from 'redux'; +import thunk from 'redux-thunk'; // Requirement for asynchronous actions +import { splitReducer, initSplitSdk } from '@splitsoftware/splitio-redux'; + +const sdkBrowserConfig = { + core: { + authorizationKey: 'YOUR_SDK_KEY', + // key represents your internal user id, or the account id that + // the user belongs to. + // This could also be a cookie you generate for anonymous users. + key: 'key' + } +}; + +// Create the Redux Store +const store = createStore( + combineReducers({ + splitio: splitReducer, + ... // Combine Split reducer with your own reducers + }), + // Add thunk middleware, used by Split SDK async actions + applyMiddleware(thunk) +); + +// Initialize the SDK by calling the initSplitSdk and passing the config in the parameters object. +store.dispatch(initSplitSdk({ config: sdkBrowserConfig })); + +export default store; +``` + + +```javascript +import { createStore, applyMiddleware, combineReducers } from 'redux'; +import thunk from 'redux-thunk'; // Requirement for asynchronous actions +import { splitReducer, initSplitSdk } from '@splitsoftware/splitio-redux'; + +const sdkNodeConfig = { + core: { + authorizationKey: 'YOUR_SDK_KEY' + } +}; +/** + * initSplitSdk should be called only once, to keep a single Split factory instance. + * The returned action is dispatched each time a new store is created, to update + * the Split status at the state. + */ +const initSplitSdkAction = initSplitSdk({ config: sdkNodeConfig }); + +const reducers = combineReducers({ + splitio: splitReducer, + ... // Combine Split reducer with your own reducers +}); + +export default function storeCreator() { + // Pass the reducers combined, including the splitReducer to each new store you create. + const store = createStore(reducers, applyMiddleware(thunk)); + // Dispatch the initSplitSdk returned action the new store instance. + store.dispatch(initSplitSdkAction); + + return store; +} +``` + + + +:::info[Notice for TypeScript] +With the SDK package on NPM, you get the SplitIO namespace, which contains useful types and interfaces. + +Feel free to dive into the declaration files if IntelliSense is not enough! +::: + +We recommend instantiating the Split factory once as a singleton and reusing it throughout your application. + +Configure the SDK with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split UI, on your Admin settings page, API keys section. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. + +## Using the SDK + +The Split SDK via its reducer keeps a portion of the store state up to date. The Split state data adheres to the following schema: + + + +```javascript +{ + // 'splitio' is the key where the Split reducer is expected to be mounted. + 'splitio': { + // The following properties indicate the current status of the SDK main client (the one bound to the key provided in the config). + 'isReady': true, // boolean flag indicating if the SDK is ready + 'isReadyFromCache': false, // boolean flag indicating if the SDK is ready from cache + 'isTimedout': false, // boolean indicating if the SDK is in a timed out state. Note: it will get ready eventually unless it's misconfigured + 'hasTimedout': false, // boolean indicating if the SDK has ever been in a timed out state + 'isDestroyed': false, // boolean indicating if the SDK has been destroyed. Read more in the shutdown section + 'lastUpdate': 56789592012, // timestamp of the last SDK state change (either timed out, got ready, destroyed or processed an update from the cloud) + + /** + * The 'treatments' property contains the evaluations of feature flags. + * Each evaluation consist of TreatmentResult objects associated to the key used on the evaluation and the feature flag name. + * We recommend that you use the provided selector functions for ease of consumption. + */ + 'treatments': { + 'feature_flag_name_1': { + 'key': { + 'treatment': 'on', + 'config': "{'copy': 'better copy', 'color': 'red'}" + } + }, + 'feature_flag_2': { + 'key': { + 'treatment': 'off', + 'config': null + } + } + }, + /** + * The 'status' property contains the status of the non-default clients, i.e., the ones that are not bound to the key provided in the config. + */ + 'status': { + 'key': { + 'isReady': true, 'isReadyFromCache': false, 'isTimedout': false, 'hasTimedout': false, 'isDestroyed': false, 'lastUpdate': 56789592012 + } + } + } +} +``` + + + +### Basic use + +When the SDK is initialized, it starts background tasks to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds depending on the size of data. If the SDK is asked to evaluate a feature flag while it's in this intermediate state, it may not have the necessary data and will queue the evaluation until it is ready. + +To make sure the SDK is fully loaded before using a treatment, wait until the SDK client is ready. You can check SDK readiness in one of the following ways: + + - Provide an `onReady` callback as a parameter to the `initSplitSdk` function. + - Check the `isReady` property of the `splitio` Redux state. + - Check if the `initSplitSdk` returned promise is resolved. + - Check the `isReady` property from the `splitio` selector result. + +After the SDK is ready, you can use the SDK to evaluate feature flags. + + + +```javascript +import { initSplitSdk } from '@splitsoftware/splitio-redux'; + +function onReadyCallback() { + // Use the SDK now that it is ready to properly evaluate. +} + +// Along with the config, you can provide a callback to be executed once the SDK is ready, to handle accordingly. +store.dispatch(initSplitSdk({ + config: sdkConfig, + onReady: onReadyCallback +})); +``` + + +```javascript +// You should have already initialized the SDK. +let isSplitReady = false; + +const handleChange = () => { + const isReadyFlag = store.getState().splitio.isReady; + + if (isReadyFlag) { + // Keep in mind that the store subscription will be triggered any time an action is dispatched, + // and some part of the state tree may potentially have changed. + isSplitReady = true; + + // Use the SDK now that it is ready to properly evaluate. + } +} + +// Note: If you're using react-redux you could do this via mapStateToProps. Read more below! +store.subscribe(handleChange); +``` + + +```javascript +import { initSplitSdk } from '@splitsoftware/splitio-redux'; + +function onReadyCallback() { + // Use the SDK now that it is ready to properly evaluate. +} + +// initSplitSdk action creator would return a promise. If the SDK is ready already the promise will be resolved by this time. +store.dispatch(initSplitSdk({ config: sdkConfig })).then(onReadyCallback); +``` + + +```javascript +import { initSplitSdk, selectStatus } from '@splitsoftware/splitio-redux'; + +// initSplitSdk action creator would return a promise. If the SDK is ready already the promise will be resolved by this time. +store.dispatch(initSplitSdk({ config: sdkConfig })); + +// Use the selector to get the isReady flag from the state. +const { isReady } = selectStatus(store.getState().splitio); +``` + + + +The `getTreatments` action creator evaluates feature flag treatments for the given `splitNames` (array of feature flag names) and `key` (e.g., user or account ID) values. In the browser, the key value is taken from the configuration and bound to the client, so you don't need to pass it here unless you need to change the key. + +If the SDK is not ready when you dispatch the `getTreatments` action, the library queues that evaluation and loads the result into the state once the `SDK_READY` event is emitted. If you happen to queue more than one evaluation for the same `splitName` and `key` the SDK will keep the latest set of attributes and evaluate only once. + + + +```javascript +import { getTreatments } from '@splitsoftware/splitio-redux'; + +// Dispatch action to evaluate and load treatments for a feature flag. The key used is the one passed in the config. +store.dispatch(getTreatments({ splitNames: ['feature_flag_1'] })); +// Or a list of feature flags. +store.dispatch(getTreatments({ splitNames: ['feature_flag_2', 'feature_flag_3'] })); +``` + + +```javascript +import { getTreatments } from '@splitsoftware/splitio-redux'; + +// Dispatch action to evaluate and load treatments for a feature flag. In Node.js we need to provide the key on each getTreatments. +store.dispatch(getTreatments({ splitNames: ['feature_flag_1'], key: 'key' })); +// Or a list of feature flags. +store.dispatch(getTreatments({ splitNames: ['feature_flag_2', 'feature_flag_3'], key: 'key' })); +``` + + + +After feature flag treatments are part of the state, use the `splitio.treatments` slice of state or our selectors to access the feature flag evaluation results and write the code for the different treatments that you defined in the Split user interface. Remember to handle the client returning control in your code. + + + +```javascript +// Import treatment value selector. +import { selectTreatmentAndStatus } from '@splitsoftware/splitio-redux'); + +// Get the treatment corresponding to the key bound to the main client for feature_flag_1 feature flag. +const { treatment, isReady } = selectTreatmentAndStatus(store.getState().splitio, 'feature_flag_1'); + +if (isReady) { + if (treatment === 'on') { + // insert on code here + } else if (treatment === 'off') { + // insert off code here + } else { + // insert control code here + } +} + +// Alternatively you could access the treatments directly from the store or your own custom selectors. +const splitTreatments = store.getState().splitio.treatments; +const treatment = splitTreatments['key']['feature_flag_1'].treatment; +``` + + +```javascript +// Import treatment value selector. +import { selectTreatmentAndStatus } from '@splitsoftware/splitio-redux'); + +// Get the treatment corresponding to the key of value 'key' for feature_flag_1 feature flag. +const { treatment } = selectTreatmentAndStatus(store.getState().splitio, 'feature_flag_1', 'key'); + +if (treatment === 'on') { + // insert on code here +} else if (treatment === 'off') { + // insert off code here +} else { + // insert control code here +} + +// Alternatively you can access the treatments directly from the store or your own custom selectors. +const splitTreatments = store.getState().splitio.treatments; +const treatment = splitTreatments['key']['feature_flag_1'].treatment; +``` + + + +Note that these treatments won't be updated automatically when there is a change to your feature flags or segments. This is to avoid flickering. If you want to react to SDK events, see the [Subscribe to events](#subscribe-to-events) section below. + +### Attribute syntax + +To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK's `getTreatments` action creator needs to be passed an attribute map at runtime. + +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatments` action creator call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in the Split user interface to decide whether to show the `on` or `off` treatment to this account. + +The SDK supports five types of attributes: strings, numbers, dates, booleans, and sets. The data type and syntax for each are: + +* **Strings:** Use type String. +* **Numbers:** Use type Number. +* **Dates: ** Use type Date and express the value in `milliseconds since epoch`.
*Note:* Milliseconds since epoch is expressed in UTC. If your date or date-time combination is in a different timezone, first convert it to UTC, then transform it to milliseconds since epoch. +* **Booleans:** Use type Boolean. +* **Sets:** Use type Array. + + + +```javascript +import { selectTreatmentAndStatus, getTreatments } from '@splitsoftware/splitio-redux'); + +const attributes = { + // date attributes are handled as `millis since epoch` + registered_date: new Date('YYYY-MM-DDTHH:mm:ss.sssZ').getTime(), + // this string will be compared against a list called `plan_type` + plan_type: 'growth', + // this number will be compared agains a const value called `deal_size` + deal_size: 10000, + // this boolean will be compared against a const value called `paying_customer` + paying_customer: true, + // this array will be compared against a set called `permissions` + permissions: ["read", "write"] +}; + +// You can pass the attributes with any getTretments action using the `attributes` key of the parameters. +store.dispatch(getTreatments({ splitNames: ['feature_flag_1'], attributes: attributes })); + +const { treatment } = selectTreatmentAndStatus(store.getState().splitio, 'feature_flag_1'); + +if (treatment === 'on') { + // insert on code here +} else if (treatment === 'off') { + // insert off code here +} else { + // insert control code here +} +``` + + +```javascript +import { selectTreatmentAndStatus, getTreatments } from '@splitsoftware/splitio-redux'); + +const attributes = { + // date attributes are handled as `millis since epoch` + registered_date: new Date('YYYY-MM-DDTHH:mm:ss.sssZ').getTime(), + // this string will be compared against a list called `plan_type` + plan_type: 'growth', + // this number will be compared agains a const value called `deal_size` + deal_size: 10000, + // this boolean will be compared against a const value called `paying_customer` + paying_customer: true, + // this array will be compared against a set called `permissions` + permissions: ["read", "write"] +}; + +// You can pass the attributes with any getTretments action using the `attributes` key of the parameters. +store.dispatch(getTreatments({ splitNames: ['feature_flag_1'], key: 'key', attributes: attributes })); + +const { treatment } = selectTreatmentAndStatus(store.getState().splitio, 'feature_flag_1', 'key'); + +if (treatment === 'on') { + // insert on code here +} else if (treatment === 'off') { + // insert off code here +} else { + // insert control code here +} +``` + + + +### Multiple evaluations at once + +If you want to evaluate treatments for multiple feature flags at once, you can pass a list of feature flag names to the `getTreatments` action creator. + +You can also evaluate multiple feature flags at once using flag sets. In that case, you can use the `flagSets` property instead of the `splitNames` property when calling the `getTreatments` action creator. Like `splitNames`, the `flagSets` property must be an array of string, each one corresponding to a different flag set name. + + + +```javascript +store.dispatch(getTreatments({ splitNames: ['FEATURE_FLAG_NAME_1', 'FEATURE_FLAG_NAME_2'] })); +``` + + +```javascript +store.dispatch(getTreatments({ flagSets: ['frontend', 'client_side'] })); +``` + + + +For retrieving the treatments from the store, you can use the `selectTreatmentAndStatus` selector. Note that this selector retrieves a single treatment value for a given feature flag name, so you need to call it for each feature flag name. + + + + +```javascript +// Getting treatments from the store +const treatments = { + FEATURE_FLAG_NAME_1: selectTreatmentAndStatus(store.getState().splitio, 'FEATURE_FLAG_NAME_1').treatment, + FEATURE_FLAG_NAME_2: selectTreatmentAndStatus(store.getState().splitio, 'FEATURE_FLAG_NAME_2').treatment +}; +``` + + + +### Get Treatments with Configurations + +To [leverage dynamic configurations with your treatments](https://help.split.io/hc/en-us/articles/360026943552), you don't need to call a specific action creator for your evaluations. Instead, our SDK stores both the treatment and the associated config (or null if there isn't one) in the Redux state. To access this values you can either use the `selectTreatmentWithConfigAndStatus` selector (recommended) or just access the config from the state. + +Each evaluation entry loaded into the state under the `treatments` key will have the structure below: + + + +```typescript +type TreatmentWithConfig = { + treatment: string, + config: string | null +}; +``` + + + +As you can see from the object structure, the config will be a stringified version of the configuration JSON defined in the Split user interface. If there is no configuration defined for a treatment, the SDK will return `null` for the config parameter. + +The `selectTreatmentWithConfigAndStatus` selector takes the exact same set of arguments as `selectTreatmentAndStatus`, as shown below. + + + +```javascript +// Import treatment with config selector. +import { selectTreatmentWithConfigAndStatus } from '@splitsoftware/splitio-redux'); + +// Get the TreatmentResult corresponding to the key bound to the client (which value is 'key' on this snippet) for 'feature_flag_1' feature flag. +const treatmentResult = selectTreatmentWithConfigAndStatus(store.getState().splitio, 'feature_flag_1').treatment; +const config = JSON.parse(treatmentResult.config); +const treatment = treatmentResult.treatment; + +if (treatment === 'on') { + // insert on code here and use configs here as necessary +} else if (treatment === 'off') { + // insert off code here and use configs here as necessary +} else { + // insert control code here +} + + +// Alternatively you could access the TreatmentResults directly from the store or your own custom selectors. +const splitTreatments = store.getState().splitio.treatments; +const treatmentResult = splitTreatments['key']['feature_flag_1']; +``` + + +```javascript +// Import treatment with config selector. +import { selectTreatmentWithConfigAndStatus } from '@splitsoftware/splitio-redux'); + +// Get the TreatmentResult corresponding to the key of value 'key' for 'feature_flag_1' feature flag. +const treatmentResult = selectTreatmentWithConfigAndStatus(store.getState().splitio, 'feature_flag_1', 'key').treatment; +const config = JSON.parse(treatmentResult.config); +const treatment = treatmentResult.treatment; + +if (treatment === 'on') { + // insert on code here and use configs here as necessary +} else if (treatment === 'off') { + // insert off code here and use configs here as necessary +} else { + // insert control code here +} + + +// Alternatively you could access the TreatmentResults directly from the store or your own custom selectors. +const splitTreatments = store.getState().splitio.treatments; +const treatmentResult = splitTreatments['key']['feature_flag_1']; +``` + + + +### Shutdown + +Call the `destroySplitSdk` function to gracefully shutdown the Split SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. +This function can be used as an action creator to update the `splitio` slice. + + + +```javascript +import { destroySplitSdk } from '@splitsoftware/splitio-redux'; + +// you can dispatch the action to update the status at the store +store.dispatch(destroySplitSdk()); + +// the dispatched Thunk action returns a promise available as the return value of dispatch itself. +// this promise always resolves once the SDK has been shutdown +store.dispatch(destroySplitSdk()).then(() => { + console.log(store.getState().splitio.isDestroyed); // prints `true` +}); +``` + + +```javascript +import { destroySplitSdk } from '@splitsoftware/splitio-redux'; + +function serverClose() { + // on scenarios where you need to destroy the SDK without dispatching the action, such as on server-side, + // you can attach a callback that is invoked once the SDK has been shutdown + destroySplitSdk({ + onDestroy: function() { + console.log("Split destroyed"); + } + }); + +}); + +``` + + + +After `destroySplitSdk()` is dispatched and resolved, the evaluated treatments at the store will keep their current treatment values. However, if there is any subsequent attempt to use the `getTreatments` action creator, the treatments will be updated to `control` to be consistent with the JavaScript SDK. Also, the SDK [manager](#manager) methods will result in empty values. + +:::warning[Important!] +Destroying the SDK is meant to be definitive. A call to the `destroySplitSdk` function also destroys the factory object. Attempting to restart the destroyed SDK by using the `initSplitSdk` action creator is not recommended and might lead to unexpected behavior. +::: + +## Track + +You can use the `track` method to record any actions your users perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users’ actions and metrics. Go to [Events](https://help.split.io/hc/en-us/articles/360020585772) to learn more about using track events in feature flags. + +The `track` method takes a params object with up to five arguments. The data type and syntax for each are: + +* **key:** The `key` variable used in the `getTreatments` call and firing this track event. The expected data type is **String**. +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: + * Contains 63 characters or fewer. + * Starts with a letter or number. + * Contains only letters, numbers, hyphen, underscore, or period. + * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` +* **VALUE:** (Optional) The value to be used in creating the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is **Integer** or **Float**. +* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. + +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event to be sent back to Split's servers on the next event post. The SDK returns `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method has been provided. + +It is important to mention that this method does not interact with the Redux store. It's only an abstraction on top of the underlying SDK track method, so you can only import one Split package. + + + +```javascript +import { track } from '@splitsoftware/splitio-redux'; + +const eventProperties = {package : "premium", admin : true, discount : 50}; + +// On the client side, SDK clients are bound to a key. If you don't provide a key, the SDK will use the key provided in the config. You can provide a different key to the track method if you want to track an event for a different key, like an account ID for example. +function track: ( + params: { key?: SplitIO.SplitKey, trafficType: string, eventType: string, value?: number, properties?: SplitIO.Properties } +) => boolean; + +// Example with both a value and properties +const queued = track({ trafficType: 'user', eventType: 'page_load_time', value: 83.334, properties: eventProperties }); +// Example with only properties +const queued = track({ trafficType: 'user', eventType: 'page_load_time', properties: eventProperties }); +// Most basic event you can track would require trafficType and eventType (just skip the value or properties params if you don't have any associated with your event) +const queued = track({ trafficType: 'user', eventType: 'page_load_time' }); + +// Example for a different key than the one provided in the SDK config +const queued = track({ key: ACCOUNT_ID, trafficType: 'account', eventType: 'page_load_time' }); +``` + + +```javascript +import { track } from '@splitsoftware/splitio-redux'; + +const eventProperties = {package : "premium", admin : true, discount : 50}; + +// On the server side the client is not bound to any key, so you need to provide these on the track call. +function track: ( + params: { key: SplitIO.SplitKey, trafficType: string, eventType: string, value?: number, properties?: SplitIO.Properties } +) => boolean; + +// Example with both a value and properties +const queued = track({ key: USER_ID, trafficType: 'user', eventType: 'page_load_time', value: 83.334, properties: eventProperties }); +// Example with only properties +const queued = track({ key: USER_ID, trafficType: 'user', eventType: 'page_load_time', properties: eventProperties }); +// Most basic event you can track would require key, trafficType and eventType (just skip the value or properties params if you don't have any associated with your event) +const queued = track({ key: USER_ID, trafficType: 'user', eventType: 'page_load_time' }); +``` + + + +## Configuration + +The SDK has a number of parameters for configuring performance. Each parameter is set to a reasonable default. However, you can override these value in the `config` object passed to the `initSplitSdk` action creator, as shown in the [Initialization](#initialization) section above. + +To learn about all the available configuration options, go to the [JavaScript SDK Configuration section.](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#configuration) + +## Localhost mode + +For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To do this, start the Split SDK in **localhost** mode (also called off-the-grid or offline mode). In this mode, the SDK neither polls or updates from Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the features. + +When instantiating the SDK in localhost mode, your `authorizationKey` is `"localhost"`. Define the feature flags you want to use in the `features` object map. All feature flag evaluations with `getTreatments` actions return the one treatment (and config, if defined) that you have defined in the map. You can then change the treatment as necessary for your testing. If you want to update a treatment or a config, or to add or remove feature flags from the mock cache, update the properties of the `features` object you've provided. The SDK simulates polling for changes and updates from it. Do not assign a new object to the `features` property because the SDK has a reference to the original object and will not detect the change. + +Any feature flag that is not provided in the `features` map returns the [control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment) if the SDK is asked to evaluate them. Use the following additional configuration parameters when instantiating the SDK in `localhost` mode: + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| scheduler.offlineRefreshRate | The refresh interval for the mocked feature flags treatments. | 15 | +| features | A fixed mapping of which treatment to show for our mocked feature flags. | {}
By default we have no mocked feature flags. | + +To use the SDK in localhost mode, replace the SDK Key with "localhost", as shown in the following test example. Note that you can define the object between a feature flag name and treatment directly or use a map to define both a treatment and a dynamic configuration. + +If you define just a string as the value for a feature flag name, the config returned by the SDK is null. If you use a map, the SDK returns the specified treatment and the specified config, which can also be null. + + + + +```javascript +const config = { + core: { + authorizationKey: 'localhost', + key: 'user_id' + }, + features: { + 'reporting_v2': 'on', // example with just a string value for the treatment + 'billing_updates': { treatment: 'visa', config: '{ "color": "blue" }' }, // example of a defined config + 'show_status_bar': { treatment: 'off', config: null } // example of a null config + } +}; + +store.dispatch(initSplitSdk({ config: sdkConfig })); +``` + + + +For a complete unit test example using Jest and React Testing Library, check [App.test.js](https://github.com/splitio/react-redux-sdk-examples/blob/master/src/__tests__/App.js). + +## Manager + +Use the Split Manager to get a list of feature flags available to the Split client. The Manager uses the data fetched from Split servers upon SDK initialization, so you should wait for the SDK to be ready after initialization (as explained in the [basic use](#basic-use) section) before using the manager; otherwise, the manager functions will return null values and empty arrays. + +You can access the manager functionality through the exposed `getSplitNames`, `getSplit`, and `getSplits` helper functions, as explained below. + + + +```typescript +import { getSplitNames, getSplit, getSplits } from '@splitsoftware/splitio-redux' + +/** + * Returns the names of feature flags registered with the SDK. + * + * @return a List of Strings of the feature flag names. + */ +const splitNamesList: SplitIO.SplitNames = getSplitNames(); + +/** + * Returns the feature flag registered with the SDK of this name. + * + * @return SplitView or null. + */ +const splitView: SplitIO.SplitView = getSplit(splitName: string); + +/** + * Retrieves the feature flags that are currently registered with the SDK. + * + * @return List of SplitViews. + */ +const splitViewsList: SplitIO.SplitViews = getSplits(); + +/** + * where each SplitView object has the following shape + */ +type SplitView = { + name: string, + trafficType: string, + killed: boolean, + treatments: Array, + changeNumber: number, + configs: { + [treatmentName: string]: string + }, + defaultTreatment: string, + sets: Array +} +``` + + + +Example usage: + + + +```javascript +import { getSplitNames, initSplitSdk, getTreatments } from '@splitsoftware/splitio-redux'; + +// You need to initialize the SDK and wait for readiness to properly use the manager methods. +store.dispatch(initSplitSdk({ config: sdkBrowserConfig, onReady: onReadyCallback })); + +function onReadyCallback() { + const myFeatureFlags = getSplitNames(); + + store.dispatch(getTreatments({ splitNames: myFeatureFlags })); +} +``` + + +```javascript +import { getSplitNames, initSplitSdk, getTreatments } from '@splitsoftware/splitio-redux'; + +// You need to initialize the SDK and wait for readiness to properly use the manager methods. +const initSplitSdkAction = initSplitSdk({ config: sdkNodeConfig, onReady: onReadyCallback }); +let myFeatureNames = []; + +function onReadyCallback() { + myFeatureNames = getSplitNames(); +} + +// Remember that the actions should be dispatched per request, so the results are loaded to the store that you'll return for the requesting user/entity. +// The SDK should be ready by this point so it can evaluate immediately. +function requestHandler(params) { + store.dispatch(initSplitSdkAction); // Loads Split general data into the store. + store.dispatch(getTreatments({ key: params.key, splitNames: myFeatureNames })); // Load the treatments and configs into the store. +} +``` + + + +For more details on about using the Manager, go to [JavaScript SDK Manager](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#manager). + +## Listener + +Split SDKs send impression data back to Split servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression listener*. For that purpose, the SDK's configurations have a parameter called `impressionListener` where an implementation of `ImpressionListener` could be added. This implementation **must** define the `logImpression` method and it receives data in the following schema. + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| impression | | Object | Impression object that has the feature flag, key, treatment, label, etc. | +| attributes | Object | A map of attributes used on the evaluation (if any). | +| sdkLanguageVersion | String| The version of the SDK. In this case the language is `redux` plus the version of the underlying SDK. | + +:::info[Note] +There are two additional keys on this object, `ip` and `hostname`. They are not used on the browser. +::: + +## Implement custom impression listener + +You can implement a custom impression listener as shown in the example below. + + + +```javascript +import { initSplitSdk } from '@splitsoftware/splitio-redux'; + +function logImpression(impressionData) { + // do something with the impression data. +} + +// Create the config for the SDK factory. +const sdkBrowserConfig = { + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + impressionListener: { + logImpression: logImpression + } +}); + +store.dispatch(initSplitSdk({ config: sdkBrowserConfig })); +``` + + +```javascript +import { initSplitSdk } from '@splitsoftware/splitio-redux'; + +function logImpression(impressionData) { + // do something with the impression data. +} + +// Create the config for the SDK factory. +const sdkNodeConfig = { + core: { + authorizationKey: 'YOUR_SDK_KEY' + }, + impressionListener: { + logImpression: logImpression + } +}); + +store.dispatch(initSplitSdk({ config: sdkNodeConfig })); +``` + + + +An impression listener is called asynchronously from the corresponding evaluation, but is almost immediate. + +Even though the SDK does not fail if there is an exception in the listener, do not block the call stack. + +## Logging + +To enable SDK logging in the browser, see how the SDK Logging works on [the client side](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#logging) or [the server side](https://help.split.io/hc/en-us/articles/360020564931-Node-js-SDK#logging) depending on where you're running the SDK. + +## Advanced use cases + +This section describes advanced use cases and features provided by the Redux SDK. + +### Instantiate multiple SDK clients + +When running **on the client side** the Redux SDK client is tied to one specific key or ID at a time which usually belongs to one traffic type (for example, `user`, `account`, `organization`). This enhances performance and reduces caching data in the SDK. + +Split supports the ability to release features to multiple keys with different traffic types. With traffic types, you can release to `users` in one feature flag and `accounts` in another. Go to [Traffic type](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) to learn more. + +If you need to roll out feature flags by different traffic types, the SDK instantiates multiple clients, one for each traffic type. For example, you may want to roll out the feature flag `user-poll` by `users` and the feature flag `account-permissioning` by `accounts`. + +You can do this by providing a new key to be used when triggering evaluations or tracking events. See some examples below: + + + +```javascript +import { initSplitSdk, getTreatments, track, selectTreatmentAndStatus, selectTreatmentWithConfigAndStatus } from '@splitsoftware/splitio-redux'; + +const sdkBrowserConfig = { + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'CUSTOMER_ACCOUNT_ID', // This is the key that will be bound to the client. + } +}; + +store.dispatch(initSplitSdk({ config: sdkBrowserConfig, onReady: onReadyCallback })); + +// Regular track for bound account client (where key would be CUSTOMER_ACCOUNT_ID) +const queuedAccountEvent = track({ trafficType: 'account', eventType: 'ACCOUNT_CREATED' }); +// Tracking events with a key parameter on the client side will get a new client (or reuse it if already created) and track events for the given key +const queuedUserEvent = track({ key: 'CUSTOMER_USER_ID', trafficType: 'user', eventType: 'PAGELOAD', value: 7.86 }); + +function onReadyCallback() { + // Dispatch action to evaluate and load treatments for a feature flag (where key would be CUSTOMER_ACCOUNT_ID) + store.dispatch(getTreatments({ splitNames: ['feature_flag_1'] })); + // Providing a different key will get a new client (or reuse it if already created) and calculate treatments for this key too. + store.dispatch(getTreatments({ splitNames: ['feature_flag_2'], key: 'CUSTOMER_USER_ID' })); + + // To access the values for the different clients, you can use our selectors. + // If you're using multiple keys, you should provide the key when retrieving the data with the selectors, otherwise we'll default to the first entry found. + const accountTreatment = selectTreatmentAndStatus(store.getState().splitio, 'feature_flag_1', 'CUSTOMER_ACCOUNT_ID').treatment; + const userTreatment = selectTreatmentAndStatus(store.getState().splitio, 'feature_flag_2', 'CUSTOMER_USER_ID').treatment; + + const accountTreatmentAndConfig = selectTreatmentWithConfigAndStatus(store.getState().splitio, 'feature_flag_1', 'CUSTOMER_ACCOUNT_ID').treatment; + const userTreatmentAndConfig = selectTreatmentWithConfigAndStatus(store.getState().splitio, 'feature_flag_2', 'CUSTOMER_USER_ID').treatment; +} +``` + + + +:::info[Number of SDK instances] +While the SDK does not put any limitations on the number of instances that can be created, we strongly recommend keeping the number of SDKs down to **one** or **two**. +::: + +### Subscribe to events + +The underlying JavaScript SDK has four different events: + +* `SDK_READY_FROM_CACHE`. This event fires in client-side code if using the `LOCALSTORAGE` storage type. This event fires once the SDK is ready to evaluate treatments using the rollout plan cached in localStorage from a previous session (which might be stale). If there is data in localStorage, this event fires almost immediately, since access to localStorage is fast; otherwise, it doesn't fire. +* `SDK_READY`. This event fires once the SDK is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Split servers. +* `SDK_READY_TIMED_OUT`. This event fires if there is no cached version of your rollout plan in localStorage, and the SDK could not download the data from Split servers within the time specified by the `readyTimeout` configuration parameter. This event does not indicate that the SDK initialization was interrupted. The SDK continues downloading the rollout plan and fires the `SDK_READY` event when finished. This delayed `SDK_READY` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. +* `SDK_UPDATE`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in the Split user interface. + +Besides managing `SDK_READY` on initialization, as explained in the [basic use](#basic-use) section, you can also add callbacks for the other events as shown below: + + + +```javascript +import { initSplitSdk } from '@splitsoftware/splitio-redux'; + +function onReadyCallback() { + // Use the SDK now that it is ready to properly evaluate. +} + +function onReadyFromCacheCallback() { + // Use the SDK to evaluate using data from the local storage cache. +} + +function onTimedoutCallback() { + // Optionally handle timeout. SDK might be ready at a later point unless there's a problem on the setup. +} + +function onUpdateCallback() { + // Optionally handle SDK update event. SDK was ready and processed an update on either your feature flags or segments + // that might change the result of an evaluation. +} + +// Provide the callbacks if you're using the config. +store.dispatch(initSplitSdk({ + config: sdkConfig, + onReady: onReadyCallback, + onReadyFromCache: onReadyFromCacheCallback, + onTimedout: onTimedoutCallback, + onUpdate: onUpdateCallback; +})); +``` + + + +You can also access the readiness state of any SDK client with the `selectStatus` selector, or when retrieving treatments with the `selectTreatmentAndStatus` or `selectTreatmentWithConfigAndStatus` selectors. + + + +```javascript +import { selectStatus, selectTreatmentAndStatus } from '@splitsoftware/splitio-redux'; + +// Retrieves current status of the SDK client with USER_ID key. If no key is provided, the main client status is returned. +const { isReady, isReadyFromCache, isTimedout, hasTimedout, isDestroyed, lastUpdate } = selectStatus(store.getState().splitio, USER_ID); + +// Readiness properties are also available in the selector result. +const { isReady, isReadyFromCache, isTimedout, hasTimedout, isDestroyed, lastUpdate, treatment } = selectTreatmentAndStatus(store.getState().splitio, 'feature_flag_1', USER_ID); +``` + + + +You can also do this from the store directly. + + + +```javascript +// Accessing the status of the main client from the splitio slice of state +const { isReady, isReadyFromCache, hasTimedout, isDestroyed } = store.getState().splitio; +``` + + + +The `getTreatments` action creator accepts two optional parameters, `evalOnUpdate` and `evalOnReadyFromCache`, which are `false` by default to avoid unwanted flickering. These parameters are **only for client side** and will be ignored if set on the server side. + +When `evalOnUpdate` is explicitly set to true, the given treatment will be re-evaluated in the event of an `SDK_UPDATE` being triggered by the underlying SDK. You can use it to re-render your components whenever there is a change due to a rollout update or a feature flag being killed. + + + +```javascript + // The results for feature_flag_1 and feature_flag_2 will be re-evaluated whenever an update is processed, + // and updated in the Redux store if they changed. + // If you wanted to stop reacting to updates, dispatch the action again with the desired key, + // feature flag names an evalOnUpdate as false (to override the behavior). + store.dispatch(getTreatments({ splitNames: ['feature_flag_1', 'feature_flag_2'], evalOnUpdate: true })); +``` + + + +When `evalOnReadyFromCache` is explicitly set to true, the given treatment will be re-evaluated in the event of an `SDK_READY_FROM_CACHE` being triggered by the underlying SDK. Therefore, this param is only relevant when using 'LOCALSTORAGE' as storage type, since otherwise the event is never emitted. + +Keep in mind that if there was no cache previously loaded on the browser or the event has already fired, this parameter will take no effect. Also, consider that when evaluating from cache you might be using a stale snapshot until the SDK is ready. + + + +```javascript + // The results for feature_flag_1 and feature_flag_2 will be evaluated when the Sdk is ready or an update is processed. + // However only feature_flag_1 will be evaluated also if the Sdk is ready from cache. + store.dispatch(initSplitSdk({ config: sdkBrowserConfig, onReadyFromCache: onReadyFromCacheCallback, onReady: onReadyCallback })); + store.dispatch(getTreatments({ splitNames: ['feature_flag_1'], evalOnUpdate: true, evalOnReadyFromCache: true })); + store.dispatch(getTreatments({ splitNames: ['feature_flag_2'], evalOnUpdate: true })); + + function onReadyFromCacheCallback() { + // feature_flag_1 is different than 'control' since we instructed the SDK to evaluate once cache is loaded. + const feature_flag_1 = selectTreatmentAndStatus(store.getState().splitio, 'feature_flag_1').treatment; + // feature_flag_2 is 'control' still as it wasn't evaluated with cached data. + const feature_flag_2 = selectTreatmentAndStatus(store.getState().splitio, 'feature_flag_2').treatment; + ... + } + + function onReadyCallback() { + // both feature flags treatments should be different than 'control' given that any pending evaluation is calculated once SDK is ready + const feature_flag_1 = selectTreatmentAndStatus(store.getState().splitio, 'feature_flag_1').treatment; + const feature_flag_2 = selectTreatmentAndStatus(store.getState().splitio, 'feature_flag_2').treatment; + ... + } +``` + + + +### Usage with React + Redux + +We provide extra functionality for users of [react-redux](https://github.com/reduxjs/react-redux) with two High Order Components (HOCs). In the future we'll add more mapState functions for your convenience. + +The `connectSplit` HOC connects a given component with the `splitio` slice and `getTreatments` action creator already bound to the dispatch, so you don't need to dispatch that action yourself. + + + + +```javascript +import { connectSplit, selectTreatmentAndStatus, getSplitNames } from '@splitsoftware/splitio-redux'; + +class MyComponent extends React.Component { + constructor(props) { + super(props); + + props.getTreatments({ splitNames: ['myFeatureFlag'] })); + }; + + render() { + const { splitio } = props; + + const isMyFeatureFlagOn = selectTreatmentAndStatus(splitio, 'myFeatureFlag').treatment === 'on'; + + if (isMyFeatureFlagOn) { + return (); + } else { + return (); + } + } +} + +export default connectSplit()(MyComponent); + +// If you've mounted the Redux SDK reducer in a key of the state other than `splitio`, you need to provide +// a callback for retrieving the feature flag related slice of state. +// If not provided, it will default to using `state.splitio`. +export default connectSplit((state) => { + return state['my_key_for_split_reducer']; +})(MyComponent); +``` + + + +The `connectToggler` HOC simplifies toggling when you have a component version for "on" treatment and a different one for any other treatments (including "control"). For example: + + + + +```javascript +import { connectToggler } from '@splitsoftware/splitio-redux'; + +const ComponentOn = () => { + return (...); +} + +const ComponentDefault = () => { + return (...); +} + +// This component renders ComponentOn if 'myFeatureFlag' evaluation yielded 'on', otherwise it renders ComponentDefault +const FeatureFlagToggler = connectToggler('myFeatureFlag')(ComponentOn, ComponentDefault); + +// If you need to evaluate for a different key than the one bound to the factory config, +// you can pass it as the second param of the decorator. +const key = 'key'; +const FeatureFlagTogglerForOtherKey = connectToggler('myFeatureFlag', key)(ComponentOn, ComponentDefault); + +// If you've mounted the Redux SDK reducer in a key of the state other than `splitio`, you need to provide +// a callback for retrieving the feature flag related slice of state as the 3rd parameter. +// If not provided, it will default to using `state.splitio`. +const FeatureFlagTogglerFromCustomStateKey = connectToggler('myFeatureFlag', key, (state) => { + return state['my_key_for_split_reducer']; +})(ComponentOn, ComponentDefault); +``` + + + +### User consent + +The SDK factory allows you to disable the tracking of events and impressions until user consent is explicitly granted or declined. To learn how to configure this feature, refer to the [JavaScript SDK User consent](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#user-consent) section. + +When using the Redux SDK, you can access the underlying SDK factory instance via the `splitSdk` object, as shown below: + + + + +```javascript +import { splitSdk, initSplitSdk, ... } from '@splitsoftware/splitio-redux'; + +// `splitSdk.factory` is null until `initSplitSdk` action creator is called +store.dispatch(initSplitSdk({ config: sdkBrowserConfig })); + +splitSdk.factory.UserConsent.getStatus(); +``` + + +```typescript +import { splitSdk, initSplitSdk, ... } from '@splitsoftware/splitio-redux'; + +// `splitSdk.factory` is null until `initSplitSdk` action creator is called +store.dispatch(initSplitSdk({ config: sdkBrowserConfig })); + +(splitSdk.factory as SplitIO.IBrowserSDK).UserConsent.getStatus(); +``` + + + +## Example apps + +The following example application shows how you can integrate the React SDK into your code. + +* [React + Redux](https://github.com/splitio/react-redux-sdk-examples) diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/_category_.json b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/_category_.json new file mode 100644 index 00000000000..898691916d5 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Client-side Suites", + "collapsible": "true", + "collapsed": "true", + "className": "red", + "position": 2 +} \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/android-suite.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/android-suite.md new file mode 100644 index 00000000000..2cfa7b43cd6 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/android-suite.md @@ -0,0 +1,1388 @@ +--- +title: Android Suite +sidebar_label: Android Suite +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +This guide provides detailed information about our Android Suite, an SDK designed to harness the full power of Split. The Android Suite is built on top of the [Android SDK](https://help.split.io/hc/en-us/articles/360020343291-Android-SDK) and the [Android RUM Agent](https://help.split.io/hc/en-us/articles/18530305949837-Android-RUM-Agent), offering a unified solution, optimized for Android development. + +The Suite provides the all-encompassing essential programming interface for working with your Split feature flags, as well as capabilities for automatically tracking performance measurements and user events. Code currently using Android SDK or Android RUM Agent can be easily upgraded to Android Suite, which is designed as a drop-in replacement. + +## Language support + +This library is designed for Android applications written in Java or Kotlin and is compatible with Android SDK versions 19 and later (4.4 Kit Kat). + +## Initialization + +Set up Split in your code base with the following two steps: + +### 1. Import the Suite into your project + +Import the SDK into your project including the dependency as follows: + +```java title="Gradle" +implementation 'io.split.client:android-suite:2.0.0' +``` + +:::warning[Important] +When upgrading from Split's Android SDK and/or Android RUM Agent to Android Suite, you need to remove individual project dependencies for the SDK and Agent. The dependency for the Suite replaces these dependencies. +::: + +### 2. Instantiate the Suite and create a new Split client + +In your code, instantiate the Suite client as shown below. + + + +```java +// Split SDK key +String sdkKey = "YOUR_SDK_KEY"; + +// Build Suite configuration by default +SplitSuiteConfiguration config = SplitSuiteConfiguration.builder().build(); + +// Create a new user key to be evaluated +// key represents your internal user id, or the account id that +// the user belongs to +String matchingKey = "key"; +Key k = new Key(matchingKey); + +// Create Suite +SplitSuite suite = SplitSuiteBuilder.build(sdkKey, k, config, getApplicationContext()); + +// Get Split Client instance +SplitClient client = suite.client(); +``` + + +```kotlin +// Split SDK key +val sdkKey = "YOUR_SDK_KEY" + +// Build Suite configuration by default +val config: SplitSuiteConfiguration = SplitSuiteConfiguration.builder().build() + +// Create a new user key to be evaluated +// key represents your internal user id, or the account id that +// the user belongs to +val matchingKey = "key" +val key: Key = Key(matchingKey) + +// Create Suite +val splitFactory: SplitSuite = + SplitSuiteBuilder.build(sdkKey, key, config, applicationContext) + +// Get Split Client instance +val client: SplitClient = splitFactory.client() +``` + + + +:::warning[Important] +If you are upgrading from Split's Android RUM Agent to Android Suite and you have setup or config information for the Android RUM Agent in the `AndroidManifest.xml`, then this information will be overridden by the Suite initialization. That is why we recommended that you remove this information from `AndroidManifest.xml` when upgrading. +::: + +When the Suite is instantiated, it starts synchronizing feature flag and segment definitions from Split servers, and also starts collecting performance and user events for the configured key and its optional traffic type (which if not set, defaults to `'user'`). + +We recommend instantiating the Split factory once as a singleton and reusing it throughout your application. + +Configure the Suite with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split UI, on your Admin settings page, API keys section. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. + +## Using the Suite + +### Basic use + +When the Suite is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds depending on the size of the data. If the Suite is asked to evaluate which treatment to show to a user for a specific feature flag while in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the Suite does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). + +To make sure the Suite is properly loaded before asking it for a treatment, block until the Suite is ready, as shown below. We set the client to listen for the `SDK_READY` event triggered by the Suite before asking for an evaluation. + +After the `SDK_READY` event fires, you can use the `getTreatment` method to return the proper treatment based on the `FEATURE_FLAG_NAME` and the `key` variables you passed when instantiating the Suite. + +You can use an if-else statement as shown below and insert the code for the different treatments that you defined in the Split user interface. Remember to handle the client returning control, for example, in the final else statement. + + + +```java +client.on(SplitEvent.SDK_READY, new SplitEventTask() { + + @Override + public void onPostExecution(SplitClient client) { + // Logic in background here + } + + @Override + public void onPostExecutionView(SplitClient client) { + // Execute logic in main thread here + String treatment = client.getTreatment("FEATURE_FLAG_NAME"); + if (treatment.equals("on")) { + // insert code here to show on treatment + } else if (treatment.equals("off")) { + // insert code here to show off treatment + } else { + // insert your control treatment code here + } + } +}); +``` + + +```kotlin +client.on(SplitEvent.SDK_READY, object : SplitEventTask() { + + override fun onPostExecution(client: SplitClient) { + // Logic in background here + } + + override fun onPostExecutionView(client: SplitClient) { + // Execute main thread logic here + when (client.getTreatment("FEATURE_FLAG_NAME")) { + "on" -> { + // insert code here to show on treatment + } + "off" -> { + // insert code here to show off treatment + } + else -> { + // insert your control treatment code here + } + } + } +``` + + + +### Attribute syntax + +To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the Suite's `getTreatment` method needs to be passed an attribute map at runtime. + +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in the Split user interface to decide whether to show the `on` or `off` treatment to this account. + +The `getTreatment` method has a number of variations that are described below. Each of these additionally has a variation that takes an attributes argument, which can defines attributes of the following types: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: + +* **Strings:** Use type String. +* **Numbers:** Use type `java.lang.Long` or `java.lang.Integer`. +* **Dates:** Express the value in milliseconds since epoch. In Java, milliseconds since epoch is of type java.lang.Long. For example, the value for the registered_date attribute below is `System.currentTimeInMillis()`, which is a long. +* **Booleans:** Use type `java.lang.Boolean`. +* **Sets:** Use type `java.util.Collection`. + + + +```java +Map attributes = new HashMap(); +attributes.put("plan_type", "growth"); +attributes.put("registered_date", System.currentTimeMillis()); +attributes.put("deal_size", 1000); +attributes.put("paying_customer", true); +String[] perms = {"read", "write"}; +attributes.put("permissions",perms); + +// See client initialization above +String treatment = client.getTreatment("FEATURE_FLAG_NAME", attributes); + +if (treatment.equals("on")) { + // insert on code here +} else if (treatment.equals("off")) { + // insert off code here +} else { + // insert control code here +} +``` + + +```kotlin +val attributes = mapOf( + "plan_type" to "growth", + "registered_date" to System.currentTimeMillis(), + "deal_size" to 1000, + "paying_customer" to true, + "permissions" to arrayOf("read", "write") +) + +// See client initialization above +when (client.getTreatment("FEATURE_FLAG_NAME", attributes)) { + "on" -> { + // insert on code here + } + "off" -> { + // insert off code here + } + else -> { + // insert control code here + } +} +``` + + + +You can pass your attributes in exactly this way to the `client.getTreatments` method. + +### Binding attributes to the client + +Attributes can optionally be bound to the client at any time during the Suite lifecycle. These attributes are stored in memory and used in every evaluation to avoid the need to keep the attribute set accessible through the whole app. When an evaluation is called, the attributes provided (if any) at evaluation time are combined with the ones that are already loaded into the Suite memory, with the ones provided at function execution time taking precedence. This enables those attributes to be overridden or hidden for specific evaluations. + +An attribute is considered valid if it follows one of the types listed below: +- String +- Number +- Boolean +- Array + +The Suite validates these before storing them and if there are invalid or missing values, possibly indicating an issue, the methods return the boolean `false` and do not update any value. + +To use these methods, refer to the example below: + + + +```java +// Prepare a Map with several attributes +Map attributes = new HashMap(); +attributes.put("plan_type", "growth"); +attributes.put("registered_date", System.currentTimeMillis()); +attributes.put("deal_size", 1000); +// Now set these on the client +client.setAttributes(attributes); +// Set one attribute +boolean result = client.setAttribute("plan_type", "growth"); +// Get an attribute +Object planType = client.getAttribute("plan_type"); +// Get all attributes +Map allAttributes = client.getAllAttributes(); +// Remove an attribute +boolean result = client.removeAttribute("deal_size"); +// Remove all attributes +boolean result = client.clearAttributes(); +``` + + +```kotlin +// Set multiple attributes +client.setAttributes( + mapOf( + "plan_type" to "growth", + "registered_date" to System.currentTimeMillis(), + "deal_size" to 1000 + ) +) +// Set one attribute +val result = client.setAttribute("plan_type", "growth") +// Get an attribute +val planType = client.getAttribute("plan_type") +// Get all attributes +val allAttributes = client.allAttributes +// Remove an attribute +val result = client.removeAttribute("deal_size") +// Remove all attributes +val result = client.clearAttributes() +``` + + + +### Multiple evaluations at once + +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` from the Split client to do this. +* `getTreatments`: Pass a list of the feature flag names you want treatments for. +* `getTreatmentsByFlagSet`: Evaluate all flags that are part of the provided set name and are cached on the Suite instance. +* `getTreatmentsByFlagSets`: Evaluate all flags that are part of the provided set names and are cached on the Suite instance. + + + +```java +// Getting treatments by feature flag names +List featureFlagNames = Lists.newArrayList("FEATURE_FLAG_NAME_1", "FEATURE_FLAG_NAME_2"); +Map treatments = client.getTreatments(featureFlagNames, null); + +// Getting treatments by set +Map treatmentsByFlagSet = client.getTreatmentsByFlagSet("frontend", null); + +// Getting treatments for the union of multiple sets +List flagSets = Lists.newArrayList("frontend", "client_side"); +Map treatmentsByFlagSets = client.getTreatmentsByFlagSets(flagSets); + +// Treatments will have the following form: +// { +// "FEATURE_FLAG_NAME_1": "on", +// "FEATURE_FLAG_NAME_2": "visa" +// } +``` + + +```kotlin +// Getting treatments by feature flag names +val featureFlagNames: List = listOf("FEATURE_FLAG_NAME_1", "FEATURE_FLAG_NAME_2") +val treatments: Map = client.getTreatments(featureFlagNames, null) + +// Getting treatments by set +val treatmentsByFlagSet = client.getTreatmentsByFlagSet("frontend", null) + +// Getting treatments for the union of multiple sets +val flagSets = listOf("frontend", "client_side") +val treatmentsByFlagSets = client.getTreatmentsByFlagSets(flagSets) + +// Treatments have the following form: +// { +// "FEATURE_FLAG_NAME_1": "on", +// "FEATURE_FLAG_NAME_2": "visa" +// } +``` + + + +### Get treatments with configurations + +To [leverage dynamic configurations with your treatments](https://help.split.io/hc/en-us/articles/360026943552), you should use the `getTreatmentWithConfig` method. + +This method will return an object with the structure below: + + + +```java +SplitResult result = client.getTreatmentWithConfig("new_boxes", attributes); + +String config = result.config(); +String treatment = result.treatment(); +``` + + +```kotlin +val result: SplitResult = client.getTreatmentWithConfig("new_boxes", attributes) + +val config: String = result.config() +val treatment: String = result.treatment() +``` + + + +As you can see from the object structure, the config is a stringified version of the configuration JSON defined in the Split user interface. If there is no configuration defined for a treatment, the Suite returns `null` for the config parameter. This method takes the exact same set of arguments as the standard `getTreatment` method. See below for examples on proper usage: + + + +```java +SplitResult treatmentResult = client.getTreatmentWithConfig("FEATURE_FLAG_NAME", attributes); +String configs = treatmentResult.config(); +String treatment = treatmentResult.treatment(); + +if (treatment.equals("on")) { + // insert on code here and use configs here as necessary +} else if (treatment.equals("off")) { + // insert off code here and use configs here as necessary +} else { + // insert control code here +} +``` + + +```kotlin +val treatmentResult: SplitResult = client.getTreatmentWithConfig("FEATURE_FLAG_NAME", attributes) +val configs: String? = treatmentResult.config() +val treatment: String = treatmentResult.treatment() + +when (client.getTreatment("FEATURE_FLAG_NAME", attributes)) { + "on" -> { + // insert on code here + } + "off" -> { + // insert off code here + } + else -> { + // insert control code here + } +} +``` + + + +If you need to get multiple evaluations at once, you can also use the `getTreatmentsWithConfig` methods. These methods take the exact same arguments as the [getTreatments](#multiple-evaluations-at-once) methods but return a mapping of feature flag names to TreatmentResults instead of strings. See example usage below: + + + +```java +// Getting treatments by feature flag names +List featureFlagNames = Lists.newArrayList("FEATURE_FLAG_NAME_1", "FEATURE_FLAG_NAME_2"); +Map treatments = client.getTreatments(featureFlagNames, null); + +// Getting treatments by set +Map treatmentsByFlagSet = client.getTreatmentsByFlagSet("frontend", null); + +// Getting treatments for the union of multiple sets +List flagSets = Lists.newArrayList("frontend", "client_side"); +Map treatmentsByFlagSets = client.getTreatmentsByFlagSets(flagSets); + +// Treatments will have the following form: +// { +// "FEATURE_FLAG_NAME_1": "on", +// "FEATURE_FLAG_NAME_2": "visa" +// } +``` + + +```kotlin +// Getting treatments by feature flag names +val featureFlagNames: List = listOf("FEATURE_FLAG_NAME_1", "FEATURE_FLAG_NAME_2") +val treatments: Map = client.getTreatments(featureFlagNames, null) + +// Getting treatments by set +val treatmentsByFlagSet = client.getTreatmentsByFlagSet("frontend", null) + +// Getting treatments for the union of multiple sets +val flagSets = listOf("frontend", "client_side") +val treatmentsByFlagSets = client.getTreatmentsByFlagSets(flagSets) + +// Treatments have the following form: +// { +// "FEATURE_FLAG_NAME_1": "on", +// "FEATURE_FLAG_NAME_2": "visa" +// } +``` + + + +### Track + +Tracking events is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users' actions and metrics. See the [Events](https://help.split.io/hc/en-us/articles/360020585772) documentation for more information. + +The Suite automatically collects some RUM metrics and sends them to Split. Specifically, crashes, ANRs and app start time (see [Default events](https://help.split.io/hc/en-us/articles/18530305949837-Android-RUM-Agent#default-events)) are automatically collected by the Suite. Learn more about these and other events in the [Android RUM Agent](https://help.split.io/hc/en-us/articles/18530305949837-Android-RUM-Agent#events) documentation. + +To track custom events, you can use the `client.track()` method or the `suite.track()` method. Both methods are demonstrated in the code examples below. + +The `client.track()` method sends events **_for the identity configured on the client instance_**. This `track` method can take up to four arguments. The proper data type and syntax for each are: + +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: + * Contains 63 characters or fewer. + * Starts with a letter or number. + * Contains only letters, numbers, hyphen, underscore, or period. + * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` +* **VALUE:** (Optional) The value to be used in creating the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is **Integer** or **Float**. +* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. + +The `suite.track()` method sends events **_for all the identities_** configured on all instances of the Suite clients. For those clients that have not been configured with a traffic type, this `track` method uses the default traffic type `user`. This `track` method can take up to three of the four arguments described above: `EVENT_TYPE`, `VALUE`, and `PROPERTIES`. + +Tracking per identity using `client.track()`: + + + +```java +SplitClient client = suite.client(); + +// The expected parameters are: +client.track(TRAFFIC_TYPE, EVENT_TYPE, eventValue, eventProperties); + +// Example with both a value and properties +Map properties = new HashMap<>(); +properties.put("package", "premium"); +properties.put("admin", true); +properties.put("discount", 50); +client.track("user", "screen_load_time", 83.334, properties); + +// Example with only properties +Map properties = new HashMap<>(); +properties.put("package", "premium"); +properties.put("admin", true); +properties.put("discount", 50L); +client.track("user", "screen_load_time", properties); +``` + + +```kotlin +val client = suite.client() + +// The expected parameters are: +client.track(TRAFFIC_TYPE, EVENT_TYPE, eventValue, eventProperties) + +// Example with both a value and properties +val properties = mapOf( + "package" to "premium", + "admin" to true, + "discount" to 50L +) +client.track("user", "screen_load_time", 83.334, properties) + +// Example with only properties +val properties = mapOf( + "package" to "premium", + "admin" to true, + "discount" to 50L +) +client.track("user", "screen_load_time", properties) +``` + + + +Tracking for all identities using `suite.track()`: + + + +```java + +// The expected parameters are: +suite.track('EVENT_TYPE', eventValue, eventProperties); + +// Example with both a value and properties +Map properties = new HashMap(); +properties.put("package", "premium"); +properties.put("admin", true); +properties.put("discount", 50L); +suite.track("screen_ad_time", 83.334, properties); + +// Example with only properties +Map properties = new HashMap(); +properties.put("package", "premium"); +properties.put("admin", true); +properties.put("discount", 50L); +suite.track("screen_load_time", null, properties); +``` + + +```kotlin +// The expected parameters are: +suite.track('EVENT_TYPE', eventValue, eventProperties) + +// Example with both a value and properties +val properties = mapOf( + "package" to "premium", + "admin" to true, + "discount" to 50L +) +suite.track("screen_ad_time", 83.334, properties) + +// Example with only properties +val properties = mapOf( + "package" to "premium", + "admin" to true, + "discount" to 50L +) +suite.track("screen_load_time", null, properties); +``` + + + +The `client.track()` methods return a boolean value of `true` or `false` to indicate whether or not the Suite was able to successfully queue the event, to be sent back to Split's servers on the next event post. + +### Shutdown + +It is good practice to call the `destroy` method before your app shuts down or is destroyed, as this method gracefully shuts down the Suite by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. + + + +```java +suite.destroy(); +``` + + +```kotlin +suite.destroy() +``` + + + +After the `suite.destroy()` method is called and finishes, any subsequent invocations to `getTreatment` or manager methods result in `control` or an empty list, respectively. You can also call `destroy` on the client instance, which will stop the specific Suite client, but will keep the Suite running. + +:::warning[Important] +The `suite.destroy()` method destroys all the Suite client instances. To create a new client instance, first create a new Suite instance. +::: + +## Configuration + +The Suite has a number of knobs for configuring performance. Each knob is tuned to a reasonable default. However, you can override the value while instantiating the Suite. The parameters available for configuration are shown below in separate tables for those parameters that affect feature flagging, those that affect the Suite RUM agent, and those that affect both. + +Feature flagging parameters: + + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| featuresRefreshRate | The Suite polls Split servers for changes to feature flags at this rate (in seconds). | 3600 seconds | +| segmentsRefreshRate | The Suite polls Split servers for changes to segments at this rate (in seconds). | 1800 seconds | +| impressionsRefreshRate | The treatment log captures which customer saw what treatment (on, off, etc.) at what time. This log is periodically flushed back to Split servers. This configuration controls how quickly the cache expires after a write (in seconds). | 1800 seconds | +| telemetryRefreshRate | The Suite caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers (in seconds). | 3600 seconds (1 hour) | +| eventsQueueSize | When using the `track` method, the number of **events** to be kept in memory. | 10000 | +| eventFlushInterval | When using the `track` method, how often is the events queue flushed to Split's servers. | 1800 seconds | +| eventsPerPush | Maximum size of the batch to push events. | 2000 | +| trafficType | When using the `track` method, the default traffic type to be used. | not set | +| connectionTimeout | HTTP client connection timeout (in ms). | 10000 ms | +| readTimeout | HTTP socket read timeout (in ms). | 10000 ms | +| impressionsQueueSize | Default queue size for impressions. | 30K | +| disableLabels | Disable labels from being sent to Split backend. Labels may contain sensitive information. | true | +| proxyHost | The location of the proxy using standard URI: `scheme://user:password@domain:port/path`. If no port is provided, the Suite defaults to port 80. | null | +| ready | Maximum amount of time in milliseconds to wait before notifying a timeout. | -1 (not set) | +| synchronizeInBackground | Activates synchronization when application host is in background. | false | +| synchronizeInBackgroundPeriod | Rate in minutes in which the background synchronization would check the conditions and trigger the data fetch if those are met. Minimum rate allowed is 15 minutes. | 15 | +| backgroundSyncWhenBatteryNotLow | When set to true, synchronize in background only if battery level is not low. | true | +| backgroundSyncWhenWifiOnly | When set to true, synchronize in background only when the available connection is wifi (unmetered). When false, background synchronization takes place as long as there is an available connection. | false | +| streamingEnabled | Boolean flag to enable the streaming service as default synchronization mechanism when in foreground. In the event of an issue with streaming, the Suite will fallback to the polling mechanism. If false, the Suite will poll for changes as usual without attempting to use streaming. | true | +| syncConfig | Optional SyncConfig instance. Use it to filter specific feature flags to be synced and evaluated by the Suite. These filters can be created with the `SplitFilter::bySet` static function (recommended, flag sets are available in all tiers), or `SplitFilter::byName` static function, and appended to this config using the `SyncConfig` builder. If not set or empty, all feature flags are downloaded by the Suite. | null | +| persistentAttributesEnabled | Enables saving attributes on persistent cache which is loaded as part of the SDK_READY_FROM_CACHE flow. All functions that mutate the stored attributes map affect the persistent cache. | false | +| syncEnabled | Controls the SDK continuous synchronization flags. When `true`, a running Suite processes rollout plan updates performed in the Split user interface (default). When `false`, it fetches all data upon init, which ensures a consistent experience during a user session and optimizes resources when these updates are not consumed by the app. | true | +| impressionsMode | This configuration defines how impressions (decisioning events) are queued on the Suite. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Split; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, ALL impressions are queued and sent to Split; this is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. | `OPTIMIZED` | +| userConsent | User consent status used to control the tracking of events and impressions. Possible values are `GRANTED`, `DECLINED`, and `UNKNOWN`. See [User consent](#user-consent) for details. | `GRANTED` | +| encryptionEnabled | If set to `true`, the local database contents is encrypted. | false | +| prefix | If set, the prefix will be prepended to the database name used by the Suite. | null | +| certificatePinningConfiguration | If set, enables certificate pinning for the given domains. For details, see the [Certificate pinning](#certificate-pinning) section below. | null | + +Suite RUM agent parameters: + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| prefix | Optional prefix to append to the `eventTypeId` of the events sent to Split by the Suite RUM agent. For example, if you set the prefix to 'my-app', the event type 'error' will be sent as 'my-app.error'. | null | + +Shared parameters: + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| logLevel | Enables logging according to the level specified. Options are `NONE`, `VERBOSE`, `DEBUG`, `INFO`, `WARNING`, `ERROR`, and `ASSERT`. | `NONE` | + +To set each of the parameters defined above, use the following syntax: + + + +```java +SplitSuite suite = SplitSuiteBuilder.build( + sdkKey, + new Key("user_id"), + SplitClientConfig + .builder() + .featuresRefreshRate(30) + .segmentsRefreshRate(30) + .impressionsRefreshRate(30) + .impressionsQueueSize(30000) + .eventFlushInterval(60) + .eventsQueueSize(500) + .telemetryRefreshRate(3600) + .logLevel(SplitLogLevel.VERBOSE) + .build(), + applicationContext); +``` + + +```kotlin +val suite: SplitSuite = SplitSuiteBuilder.build( + sdkKey, + Key("user_id"), + SplitClientConfig + .builder() + .featuresRefreshRate(30) + .segmentsRefreshRate(30) + .impressionsRefreshRate(30) + .impressionsQueueSize(30000) + .eventFlushInterval(60) + .eventsQueueSize(500) + .telemetryRefreshRate(3600) + .logLevel(SplitLogLevel.VERBOSE) + .build(), + applicationContext) +``` + + + +## Localhost mode + +For testing, a developer can put code behind feature flags on their development machine without the Suite requiring network connectivity. To achieve this, you can start the Suite in **localhost** mode (aka, off-the-grid mode). In this mode, the Suite neither polls or updates Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the feature flags. To use the Suite in localhost mode, replace the SDK Key with `localhost`, as shown in the example below/ + +The format for defining the definitions is as follows: + + + +```yaml +## - feature_name: +## treatment: "treatment_applied_to_this_entry" +## keys: "single_key_or_list" +## config: "{\"desc\" : \"this applies only to ON treatment\"}" + +- my_feature: + treatment: "on" + keys: "key" + config: "{\"desc\" : \"this applies only to ON treatment\"}" +- some_other_feature: + treatment: "off" +- my_feature: + treatment: "off" +- other_feature: + treatment: "off" + keys: ["key_1", "key_2"] + config: "{\"desc\" : \"this overrides multiple keys and returns off treatment for those keys\"}" +``` + + + +In the example above, we have four entries: + + * The first entry defines that for feature flag `my_feature`, the key `key` returns the treatment `on` and the `on` treatment is tied to the configuration `{"desc" : "this applies only to ON treatment"}`. + * The second entry defines that the feature flag `some_other_feature` always returns the `off` treatment and no configuration. + * The third entry defines that `my_feature` always returns `off` for all keys that don't match another entry (in this case, any key other than `key`). + * The fourth entry shows an example on how to override a treatment for a set of keys. + +In this mode, the Split Suite loads the yaml file from a resource bundle file at the assets' project `src/main/assets/splits.yaml`. + + + +```java +import io.split.android.client.SplitClient; +import io.split.android.client.SplitFactoryBuilder; +import io.split.android.client.api.Key; + +// Create a new user key to be evaluated +String matchingKey = "key"; +Key key = new Key(matchingKey); + +SplitClient client = SplitSuiteBuilder.build("localhost", key, getApplicationContext()).client(); +``` + + +```kotlin +import io.split.android.client.SplitFactoryBuilder +import io.split.android.client.api.Key + +// Create a new user key to be evaluated +val key = Key("key") + +val client = SplitSuiteBuilder.build( + "localhost", + key, + applicationContext +).client() +``` + + + +## Manager + +Use the Split Manager to get a list of feature flags available to the Split client. To instantiate a Manager in your code base, use the same factory that you used for your client. + + + +```java +SplitFactory splitFactory = SplitFactoryBuilder.build("YOUR_API_KEY"); +SplitManager manager = splitFactory.manager(); +``` + + +```kotlin +val splitFactory: SplitFactory = SplitFactoryBuilder.build( + "api_key", + Key("key"), + applicationContext +) + +val manager: SplitManager = splitFactory.manager() +``` + + + +The Manager then has the following methods available. + + + +```java +/** + * Retrieves the feature flags that are currently registered with the + * Suite. + * + * @return a List of SplitView or empty. + */ +List splits(); + +/** + * Returns the feature flags registered with the Suite of this name. + * + * @return SplitView or null + */ +SplitView split(String SplitName); + +/** + * Returns the names of feature flags registered with the Suite. + * + * @return a List of String (Split feature names) or empty + */ +List splitNames(); +``` + + +```kotlin +/** + * Retrieves the feature flags that are currently registered with the + * Suite. + * + * @return a List of SplitView or empty. + */ +fun splits(): List + +/** + * Returns the feature flags registered with the Suite of this name. + * + * @return SplitView or null + */ +fun split(SplitName: String): SplitView? + +/** + * Returns the names of features flags registered with the Suite. + * + * @return a List of String (feature flag names) or empty + */ +fun splitNames(): List +``` + + + +The `SplitView` object referenced above has the following structure. + + + +```java +public class SplitView { + public String name; + public String trafficType; + public boolean killed; + public List treatments; + public long changeNumber; + public Map configs; + public String defaultTreatment; + public List sets; +} +``` + + +```kotlin +class SplitView( + var name: String?, + var trafficType: String?, + var killed: Boolean, + var treatments: List?, + var changeNumber: Long + var defaultTreatment: String? + var sets: List +) +``` + + + +## Listener + +The Split Suite sends impression data back to Split servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression listener*. + +The Suite sends the generated impressions to the impression listener right away. Because of this, be careful while implementing handling logic to avoid blocking the thread. Generally speaking, you should create a separate thread to handle incoming impressions. Refer to the snippet below: + + + +```java +SplitClientConfig config = SplitClientConfig.builder() + .impressionListener(new MyImpressionListener()) + .build(); + +class MyImpressionListener implements ImpressionListener { + @Override + public void log(Impression impression) { + // Do something on UI thread + new Thread(new Runnable() { + public void run() { + // Do something in another thread (use this most of the time!) + } + }).start(); + } + + @Override + public void close() { + } +} +``` + + +```kotlin +class MyImpressionListener : ImpressionListener { + override fun log(impression: Impression) { + // Do something on UI thread + Thread { + // Do something in another thread (use this most of the time!) + }.start() + } + + override fun close() { + + } +} + +val config = SplitClientConfig.builder() + .impressionListener(MyImpressionListener()) + .build() +``` + + + +In regards with the data available here, refer to the `Impression` objects interface and information about each field below: + + + +```java + String key(); + String bucketingKey(); + String split(); + String treatment(); + Long time(); + String appliedRule(); + Long changeNumber(); + Map attributes(); + Long previousTime(); +``` + + +```kotlin + key(): String? + bucketingKey(): String? + split(): String? + treatment(): String? + time(): Long? + appliedRule(): String? + changeNumber(): Long? + attributes(): Map? + previousTime(): Long? +``` + + + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| key | String | Key which is evaluated. | +| bucketingKey | String | Key which is used for bucketing, if provided. | +| split | String | Feature flag which is evaluated. | +| treatment | String | Treatment that is returned. | +| time | Long | Timestamp of when the impression is generated. | +| appliedRule | String | Targeting rule in the definition that matched resulting in the treatment being returned. | +| changeNumber | Long | Date and time of the last change to the targeting rule that the Suite used when it served the treatment. It is important to understand when a change made to a feature flag is picked up by the Suite and whether one of the Suite instances is not picking up changes. | +| attributes | Map\ | A map of attributes passed to `getTreatment`/`getTreatments`, if any. | +| previousTime | Long | If Suite is deduping and a matching impression is seen before on the lifetime of the instance this is its timestamp. | + +## Flush + +The `flush` method sends the data stored in memory (impressions and events tracked using client's `track` method) to the Split cloud and clears the successfully posted data. If a connection issue is experienced, the data is sent on the next attempt. If you want to flush all pending data when your app goes to the background, a good place to call this method is the `onPause` callback of your activity. + + + +```java +client.flush(); +``` + + +```kotlin +client.flush() +``` + + + +## Logging + +To enable logging, the `logLevel` setting is available in `SplitClientConfig` class: + +```swift title="Setup logs" +// This setting type is `SplitLogLevel`. +// The available values are DEBUG, INFO, WARNING, ERROR, ASSERT and NONE +SplitClientConfig.Builder builder = SplitClientConfig.builder() + .logLevel(SplitLogLevel.VERBOSE) +SplitClientConfig config = builder.build(); + + ... +``` + +## Advanced use cases + +This section describes advanced use cases and features provided by the Suite. + +### Instantiate multiple clients + +Split supports the ability to create multiple clients, one for each user ID. Each Suite client is tied to one specific customer ID at a time. For example, if you need to roll out feature flags for different user IDs, you can instantiate multiple clients, one for each ID. You can then evaluate them using the corresponding client. + +You can do this with the example below: + + + +```java +// Create factory +Key key = new Key("anonymous_user"); +SplitClientConfig config = SplitClientConfig.builder().build(); +SplitSuite suite = SplitSuiteBuilder.build("yourAuthKey", key, config, getApplicationContext()); +// Now when you call suite.client(), the Suite will create a client +// using the Key you passed in during the factory creation +SplitClient anonymousClient = suite.client(); +// To create another client for a user instead, just pass in a different Key or id +SplitClient userClient = suite.client("user_id"); +// Add events handler for each client to be notified when Suite is ready +anonymousClient.on(SplitEvent.SDK_READY, new SplitEventTask() { + @Override + public void onPostExecutionView(SplitClient client) { + // Check treatment for account-permissioning and anonymousClient + String accountPermissioningTreatment = anonymousClient.getTreatment("account-permissioning"); + } +}); +userClient.on(SplitEvent.SDK_READY, new SplitEventTask() { + @Override + public void onPostExecutionView(SplitClient client) { + // Check treatment for user-poll and userClient + String userPollTreatment = userClient.getTreatment("user-poll"); + } +}); +``` + + +```kotlin +// Create factory +val key = Key("anonymous_user") +val config = SplitClientConfig.builder().build() +val suite = SplitSuiteBuilder.build("yourAuthKey", key, config, getApplicationContext()) +// Now when you call suite.client(), the Suite will create a client +// using the Key you passed in during the factory creation +val anonymousClient = suite.client() +// To create another client for a user instead, just pass in a different Key or id +val userClient = suite.client("user_id") +// Add events handler for each client to be notified when Suite is ready +anonymousClient.on(SplitEvent.SDK_READY, object : SplitEventTask() { + override fun onPostExecutionView(client: SplitClient) { + // Check treatment for account-permissioning and anonymousClient + val accountPermissioningTreatment = anonymousClient.getTreatment("account-permissioning") + } +}) +userClient.on(SplitEvent.SDK_READY, object : SplitEventTask() { + override fun onPostExecutionView(client: SplitClient) { + // Check treatment for user-poll and userClient + val userPollTreatment = userClient.getTreatment("user-poll") + } +}) +``` + + + +The events captured by the Suite's RUM agent are sent to Split servers using the traffic types and keys of the created client. If no traffic type is provided, the traffic type is `user` by default. + +:::info[Number of Suite instances] +While the Suite does not put any limitations on the number of instances that can be created, we strongly recommend keeping the number of instances down to **one** or **two**. +::: + +### Subscribe to events + +You can listen for four different events from the Suite. + +* `SDK_READY_FROM_CACHE`. This event fires once the Suite is ready to evaluate treatments using a locally cached version of your rollout plan from a previous session (which might be stale). If there is data in the cache, this event fires almost immediately, since access to the cache is fast; otherwise, it doesn't fire. +* `SDK_READY`. This event fires once the Suite is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Split servers. +* `SDK_READY_TIMED_OUT`. This event fires if there is no cached version of your rollout plan in disk cache, and the Suite could not download the data from Split servers within the time specified by the `ready` setting of the `SplitClientConfig` object. This event does not indicate that the Suite initialization was interrupted. The Suite continues downloading the rollout plan and fires the `SDK_READY` event when finished. This delayed `SDK_READY` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. +* `SDK_UPDATE`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in the Split user interface. + +To define what is executed after each event, create an extension of `SplitEventTask`. + + + +```java +public class SplitEventTask { + public void onPostExecution(SplitClient client) { } + public void onPostExecutionView(SplitClient client) { } +} +``` + + +```kotlin +open class SplitEventTask { + open fun onPostExecution(client: SplitClient) { } + open fun onPostExecutionView(client: SplitClient) { } +} +``` + + + +Both the `onPostExecution` and `onPostExecutionView` methods are executed after the SDK event is triggered. The `onPostExecution` method is executed on a background thread, so we recommend it's used in cases where heavy computation is needed. The `onPostExecutionView` method is executed on the UI thread, so we recommend it's only used for short tasks or manipulating the UI. + +The syntax to listen for each event is shown below: + + + +```java +client.on(SplitEvent.SDK_READY, new SplitEventTask() { + @Override + public void onPostExecution(SplitClient client) { + // Background Code in Here + } + @Override + public void onPostExecutionView(SplitClient client) { + // UI Code in Here + } +}); +// When definitions and any bound attributes were loaded from cache +client.on(SplitEvent.SDK_READY_FROM_CACHE, new SplitEventTask() { + @Override + public void onPostExecution(SplitClient client) { + // Background Code in Here + } + @Override + public void onPostExecutionView(SplitClient client) { + // UI Code in Here + } +}); +// When the Suite couldn't fetch definitions before *config.ready* time +client.on(SplitEvent.SDK_READY_TIMED_OUT, new SplitEventTask() { + @Override + public void onPostExecution(SplitClient client) { + // Background Code in Here + } + @Override + public void onPostExecutionView(SplitClient client) { + // UI Code in Here + } +}); +// When definitions have changed +client.on(SplitEvent.SDK_READY_UPDATE, new SplitEventTask() { + @Override + public void onPostExecution(SplitClient client) { + // Background Code in Here + } + @Override + public void onPostExecutionView(SplitClient client) { + // UI Code in Here + } +}); +``` + + +```kotlin +client.on(SplitEvent.SDK_READY, object : SplitEventTask() { + override fun onPostExecution(client: SplitClient) { + // Background Code in Here + } + override fun onPostExecutionView(client: SplitClient) { + // UI Code in Here + } +}) +// When definitions were loaded from cache +client.on(SplitEvent.SDK_READY_FROM_CACHE, object : SplitEventTask() { + override fun onPostExecution(client: SplitClient) { + // Background Code in Here + } + override fun onPostExecutionView(client: SplitClient) { + // UI Code in Here + } +}) +// When the Suite couldn't fetch definitions before *config.ready* time +client.on(SplitEvent.SDK_READY_TIMED_OUT, object : SplitEventTask() { + override fun onPostExecution(client: SplitClient) { + // Background Code in Here + } + override fun onPostExecutionView(client: SplitClient) { + // UI Code in Here + } +}) +// When definitions have changed +client.on(SplitEvent.SDK_UPDATE, object : SplitEventTask() { + override fun onPostExecution(client: SplitClient) { + // Background Code in Here + } + override fun onPostExecutionView(client: SplitClient) { + // UI Code in Here + } +}) +``` + + + +### User consent + +The Suite allows you to disable the tracking of events and impressions until user consent is explicitly granted or declined. + +The `userConsent` configuration parameter lets you set the initial consent status of the Suite instance, and the Suite method `UserConsent.setStatus(boolean)` lets you grant (enable) or decline (disable) the dynamic data tracking. + +There are three possible initial states: + * `'GRANTED'`: the user grants consent for tracking events and impressions. The Suite sends them to Split cloud. This is the default value if `userConsent` param is not defined. + * `'DECLINED'`: the user declines consent for tracking events and impressions. The Suite does not send them to Split cloud. + * `'UNKNOWN'`: the user neither grants nor declines consent for tracking events and impressions. The Suite tracks them in its internal storage, and eventually either sends them or not if the consent status is updated to `'GRANTED'` or `'DECLINED'` respectively. The status can be updated at any time with the `setUserConsent` Suite method. + + + +```java +// Overwrites the initial consent status of the Suite instance, which is 'GRANTED' by default. +// 'UNKNOWN' status represents that the user has neither granted nor declined consent for tracking data, +// so the Suite locally tracks data but not send it to Split cloud until consent is changed to 'GRANTED'. +SplitClientConfig config = SplitClientConfig.builder() + .userConsent(UserConsent.UNKNOWN) + .build(); +SplitSuite suite = SplitSuiteBuilder.build("YOUR_SDK_KEY", + new Key(mUserKey, null), + config, context); +// Changed User Consent status to 'GRANTED'. Data is sent to Split cloud. +suite.setUserConsent(true); +// Changed User Consent status to 'DECLINED'. Data is not sent to Split cloud. +suite.setUserConsent(false); +// The 'getUserConsent' method returns User Consent status. +// We expose the constants for customer checks and tracking. +if (suite.getUserConsent() == UserConsent.DECLINED) { + Log.i(TAG, "USER CONSENT DECLINED"); +} else if (suite.getUserConsent() == UserConsent.GRANTED) { + Log.i(TAG, "USER CONSENT GRANTED"); +} else if (suite.getUserConsent() == UserConsent.UNKNOWN) { + Log.i(TAG, "USER CONSENT UNKNOWN"); +} +``` + + +```kotlin +// Overwrites the initial consent status of the Suite instance, which is 'GRANTED' by default. +// 'UNKNOWN' status represents that the user has neither granted nor declined consent for tracking data, +// so the Suite locally tracks data but not send it to Split cloud until consent is changed to 'GRANTED'. +val config: SplitClientConfig = SplitClientConfig.builder() + .userConsent(UserConsent.UNKNOWN) + .build() +val suite: SplitSuite = SplitSuiteBuilder.build("YOUR_SDK_KEY", + Key(mUserKey, null), + config, context) +// Changed User Consent status to 'GRANTED'. Data is sent to Split cloud. +suite.setUserConsent(true) +// Changed User Consent status to 'DECLINED'. Data is not sent to Split cloud. +suite.setUserConsent(false) +// The 'getUserConsent' method returns User Consent status. +// We expose the constants for customer checks and tracking. +if (suite.getUserConsent() == UserConsent.DECLINED) { + Log.i(TAG, "USER CONSENT DECLINED") +} else if (suite.getUserConsent() == UserConsent.GRANTED) { + Log.i(TAG, "USER CONSENT GRANTED") +} else if (suite.getUserConsent() == UserConsent.UNKNOWN) { + Log.i(TAG, "USER CONSENT UNKNOWN") +} +``` + + + +### Certificate pinning + +The SDK allows you to constrain the certificates that the SDK trusts, using one of the following techniques: + +1. Pin a certificate's `SubjectPublicKeyInfo`, by providing the public key as a ___base64 SHA-256___ hash or a ___base64 SHA-1___ hash. +2. Pin a certificate's entire certificate chain (the root, all intermediate, and the leaf certificate), by providing the certificate chain as a .der file. + +Each pin corresponds to a host. For subdomains, you can optionally use wildcards, where `*` will match one subdomain (e.g. `*.example.com`), and `**` will match any number of subdomains (e.g `**.example.com`). + +You can optionally configure a listener to execute on certificate validation failure for a host. + +To set the SDK to require pinned certificates for specific hosts, add the `CertificatePinningConfiguration` object to `SplitClientConfig.Builder`, as shown below. + + + +```java +import io.split.android.client.network.CertificatePinningConfiguration; +import io.split.android.client.SplitClientConfig; +import com.yourApp.R; // to reference your res/ folder + +// Define pins for certificate pinning +CertificatePinningConfiguration certPinningConfig = CertificatePinningConfiguration.builder() + + // Provide a base 64 SHA-256 hash + .addPin("*.example1.com", "sha256/7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=") + + // Provide a certificate chain as a 'res/raw/cert.der' file + .addPin("*.example2.com", context.getResources().openRawResource(R.raw.cert)) + + // Provide a listener to log failure + .failureListener((host, certificateChain) -> { + Log.d("CertPinning", "Certificate pinning failure for " + host); + }) + + .build(); + +// Set the CertificatePinningConfiguration property for the Split client configuration +SplitClientConfig config = SplitClientConfig.builder() + .certificatePinningConfiguration(certPinningConfig) + // you can add other configuration properties here + .build(); + +``` + + +```kotlin +import io.split.android.client.network.CertificatePinningConfiguration +import io.split.android.client.SplitClientConfig +import com.yourApp.R // to reference your res/ folder + +// Define pins for certificate pinning +val certPinningConfig = CertificatePinningConfiguration.builder() + + // Provide a base 64 SHA-256 hash + .addPin("*.example1.com", "sha256/7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=") + + // Provide a certificate chain as a 'res/raw/cert.der' file + .addPin("*.example2.com", context.getResources().openRawResource(R.raw.cert)) + + // Provide a listener to log failure + .failureListener { host, certificateChain -> + Log.d("CertPinning", "Certificate pinning failure for $host") + } + + .build() + +// Set the CertificatePinningConfiguration property for the Split client configuration +val config = SplitClientConfig.builder() + .certificatePinningConfiguration(certPinningConfig) + // you can add other configuration properties here + .build() + +``` + + + +### RUM agent configuration + +The Suite handles the setup of its RUM agent using the same SDK key. Configurations for [Logging](#logging) and [Identities](#instantiate-multiple-clients) are also shared with the Suite's RUM agent. + +You can further configure the RUM agent passing a `SplitSuiteConfiguration` object instead of `SplitClientConfiguration`. To create this object, use a `SplitSuiteConfigurationBuilder`. + + + +```java +// Optionally create feature flagging configuration +SplitClientConfig splitClientConfig = SplitClientConfig + .builder() + .logLevel(SplitLogLevel.VERBOSE) + .build(); + +// Create a SplitSuiteConfiguration builder +SplitSuiteConfigurationBuilder suiteConfigurationBuilder = SplitSuiteConfiguration.builder(); + +// Specify a prefix for the Suite events +suiteConfigurationBuilder = suiteConfigurationBuilder.setPrefix("myprefix_"); + +// Create the SplitSuiteConfiguration, passing the optional SplitClientConfig for feature flagging +SplitSuiteConfiguration splitSuiteConfiguration = suiteConfigurationBuilder + .build(splitClientConfig); + +// Instantiate the Suite passing in the SplitSuiteConfiguration +SplitSuite suite = SplitSuiteBuilder.build( + "YOUR_SDK_KEY", + new Key("key1"), + splitSuiteConfiguration, + applicationContext); +``` + + +```kotlin +// Optionally create feature flagging configuration +val splitClientConfig: SplitClientConfig = SplitClientConfig + .builder() + .logLevel(SplitLogLevel.VERBOSE) + .build() + +// Create a SplitSuiteConfiguration builder +var suiteConfigurationBuilder = SplitSuiteConfiguration.builder() + +// Specify a prefix for the Suite events +suiteConfigurationBuilder = suiteConfigurationBuilder.setPrefix("myprefix_") + +// Create the SplitSuiteConfiguration, passing the optional SplitClientConfig for feature flagging +val splitSuiteConfiguration: SplitSuiteConfiguration = suiteConfigurationBuilder + .build(splitClientConfig) + +// Instantiate the Suite passing in the SplitSuiteConfiguration +val suite: SplitSuite = SplitSuiteBuilder.build( + "YOUR_SDK_KEY", + Key("key1"), + splitSuiteConfiguration, + applicationContext +) +``` + + \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/browser-suite.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/browser-suite.md new file mode 100644 index 00000000000..5068ec7fc52 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/browser-suite.md @@ -0,0 +1,1255 @@ +--- +title: Browser Suite +sidebar_label: Browser Suite +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +This guide provides detailed information about our JavaScript Browser Suite, an SDK designed to harness the full power of Split. The Browser Suite is built on top of the [Browser SDK](https://help.split.io/hc/en-us/articles/360058730852-Browser-SDK) and the [Browser RUM Agent](https://help.split.io/hc/en-us/articles/360030898431-Browser-RUM-agent), offering a unified solution, optimized for web development. + +The Suite provides the all-encompassing essential programming interface for working with your Split feature flags, as well as capabilities for automatically tracking performance measurements and user events. Code currently using Browser SDK or Browser RUM Agent can be easily upgraded to Browser Suite, which is designed as a drop-in replacement. + +## Language support + +The JavaScript Browser Suite supports all major browsers. While the library was built to support ES5 syntax, it depends on native support for ES6 Promise. If the Promise object is not available in your target browsers, you will need an ES6 Promise polyfill. + +## Initialization + +Set up Split in your code base with the following two steps: + +### 1. Import the Suite into your project + +You can import the Suite into your project by installing the NPM package. + + + +```bash +npm install --save @splitsoftware/browser-suite +``` + + +```bash +yarn add @splitsoftware/browser-suite +``` + + +```html + +``` + + + +:::info[NPM package is recommended over CDN bundle] +We strongly recommend installing the SDK via NPM or your package manager of choice. The package brings out-of-the-box IntelliSense, TypeScript declarations and tree-shaking support, allowing you to take advantage of optimizations offered by modern module bundlers like Webpack and Rollup. + +We also support a prebuilt bundle distributed via CDN. This is particularly useful for quick tests, PoC's or very specific use cases, but it is a large file and may slow down your page load time. +::: + +### 2. Instantiate the Suite and create a new Split client + +In your code, instantiate the Suite client as shown below. + + + +```javascript +import { SplitSuite } from '@splitsoftware/browser-suite'; + +// Instantiate the Suite +const suite = SplitSuite({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + // key represents your internal user id, or the account id that + // the user belongs to. + // This could also be a cookie you generate for anonymous users + key: 'key' + } +}); + +// And get the client instance you'll use +const client = suite.client(); +``` + + +```javascript +var SplitSuite = require('@splitsoftware/browser-suite').SplitSuite; + +// Instantiate the Suite +var suite = SplitSuite({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + // key represents your internal user id, or the account id that + // the user belongs to. + // This could also be a cookie you generate for anonymous users + key: 'key' + } +}); + +// And get the client instance you'll use +var client = suite.client(); +``` + + +```javascript +// Instantiate the Suite. CDN exposes a splitio object globally, +// with a reference to the SplitSuite (as well as any extra modules) + +var suite = splitio.SplitSuite({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + // key represents your internal user id, or the account id that + // the user belongs to. + // This could also be a cookie you generate for anonymous users + key: 'key' + } +}); + +// And get the client instance you'll use +var client = suite.client(); +``` + + + +When the Suite is instantiated, it starts synchronizing feature flag and segment definitions from Split servers, and also starts collecting performance and user events for the configured key and its optional traffic type (which if not set, defaults to `'user'`). + +We recommend instantiating the Suite once as a singleton and reusing it throughout your application. + +Configure the Suite with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split UI, on your Admin settings page, API keys section. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. + +## Using the Suite + +### Basic use + +When the Suite is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds depending on the size of the data. If the Suite is asked to evaluate which treatment to show to a user for a specific feature flag while in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the Suite does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). + +To make sure the Suite is properly loaded before asking it for a treatment, block until the Suite is ready, as shown below. We set the client to listen for the `SDK_READY` event triggered by the Suite before asking for an evaluation. + +After the `SDK_READY` event fires, you can use the `getTreatment` method to return the proper treatment based on the `FEATURE_FLAG_NAME` and the `key` variables you passed when instantiating the Suite. + +You can use an if-else statement as shown below and insert the code for the different treatments that you defined in the Split user interface. Remember to handle the client returning control, for example, in the final else statement. + + + + +```javascript +client.on(client.Event.SDK_READY, function() { + var treatment = client.getTreatment('FEATURE_FLAG_NAME'); + + if (treatment == "on") { + // insert code here to show on treatment + } else if (treatment == "off") { + // insert code here to show off treatment + } else { + // insert your control treatment code here + } +}); +``` + + +```javascript +client.on(client.Event.SDK_READY, function() { + const treatment: SplitIO.Treatment = client.getTreatment('FEATURE_FLAG_NAME'); + + if (treatment == "on") { + // insert code here to show on treatment + } else if (treatment == "off") { + // insert code here to show off treatment + } else { + // insert your control treatment code here + } +}); +``` + + + +### Attribute syntax + +To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the Suite's `getTreatment` method needs to be passed an attribute map at runtime. + +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in the Split user interface to decide whether to show the `on` or `off` treatment to this account. + +The `getTreatment` method has a number of variations that are described below. Each of these additionally has a variation that takes an attributes argument, which can defines attributes of the following types: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: + +* **Strings:** Use type String. +* **Numbers:** Use type Number. +* **Dates:** Use type Date and express the value in `milliseconds since epoch`.
**Note:** Milliseconds since epoch is expressed in UTC. If your date or date-time combination is in a different timezone, first convert it to UTC, then transform it to milliseconds since epoch. +* **Booleans:** Use type Boolean. +* **Sets:** Use type Array. + + + + +```javascript +var attributes = { + // date attributes are handled as `millis since epoch` + registered_date: new Date('YYYY-MM-DDTHH:mm:ss.sssZ').getTime(), + // this string will be compared against a list called `plan_type` + plan_type: 'growth', + // this number will be compared agains a const value called `deal_size` + deal_size: 10000, + // this boolean will be compared against a const value called `paying_customer` + paying_customer: true, + // this array will be compared against a set called `permissions` + permissions: ["read", "write"] +}; + +var treatment = client.getTreatment('FEATURE_FLAG_NAME', attributes); + +if (treatment === 'on') { + // insert on code here +} else if (treatment === 'off') { + // insert off code here +} else { + // insert control code here +} +``` + + +```javascript +const attributes: SplitIO.Attributes = { + // date attributes are handled as `millis since epoch` + registered_date: new Date('YYYY-MM-DDTHH:mm:ss.sssZ').getTime(), + // this string will be compared against a list called `plan_type` + plan_type: 'growth', + // this number will be compared agains a const value called `deal_size` + deal_size: 10000, + // this array will be compared against a set called `permissions` + permissions: ['read', 'write'] +}; + +const treatment: SplitIO.Treatment = client.getTreatment('FEATURE_FLAG_NAME', attributes); + +if (treatment === 'on') { + // insert on code here +} else if (treatment === 'off') { + // insert off code here +} else { + // insert control code here +} +``` + + + +You can pass your attributes in exactly this way to the `client.getTreatments` method. + +### Binding attributes to the client + +Attributes can optionally be bound to the client at any time during the Suite lifecycle. These attributes are stored in memory and used in every evaluation to avoid the need to keep the attribute set accessible through the whole app. When an evaluation is called, the attributes provided (if any) at evaluation time are combined with the ones that are already loaded into the Suite memory, with the ones provided at function execution time taking precedence. This enables those attributes to be overridden or hidden for specific evaluations. + +An attribute is considered valid if it follows one of the types listed below: +- String +- Number +- Boolean +- Array + +The Suite validates these before storing them and if there are invalid or missing values, possibly indicating an issue, the methods return the boolean `false` and do not update any value. + +To use these methods, refer to the example below: + + + + +```javascript +var attributes = { + registered_date: new Date('YYYY-MM-DDTHH:mm:ss.sssZ').getTime(), + plan_type: 'growth', + deal_size: 10000, + paying_customer: true, + permissions: ["read", "write"] +}; + +// Set attributes returns true unless there is an issue storing it +var result = client.setAttributes(attributes); + +// Set one attribute and returns true unless there is an issue storing it +var result = client.setAttribute('paying_customer', false); + +// Get an attribute +var plan_type = client.getAttribute('plan_type'); + +// Get all attributes +var stored_attributes = client.getAttributes(); + +// Remove an attribute +var result = client.removeAttribute('permissions'); + +// Remove all attributes +var result = client.clearAttributes(); + +``` + + + +### Multiple evaluations at once + +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` from the Split client to do this. +* `getTreatments`: Pass a list of the feature flag names you want treatments for. +* `getTreatmentsByFlagSet`: Evaluate all flags that are part of the provided set name and are cached on the Suite instance. +* `getTreatmentsByFlagSets`: Evaluate all flags that are part of the provided set names and are cached on the Suite instance. + + + + +```javascript +// Getting treatments by feature flag names +var flagNames = ['FEATURE_FLAG_NAME_1', 'FEATURE_FLAG_NAME_2']; +var treatments = client.getTreatments(flagNames); + +// Getting treatments by set +treatments = client.getTreatmentsByFlagSet('frontend'); + +// Getting treatments for the union of multiple sets +var flagSets = ['frontend', 'client_side']; +treatments = client.getTreatmentsByFlagSets(flagSets); + +// treatments have the following form: +// { +// FEATURE_FLAG_NAME_1: 'on', +// FEATURE_FLAG_NAME_2: 'visa' +// } +``` + + + +```javascript +// Getting treatments by feature flag names +const flagNames = ['FEATURE_FLAG_NAME_1', 'FEATURE_FLAG_NAME_2']; +let treatments: SplitIO.Treatments = client.getTreatments(flagNames); + +// Getting treatments by set +treatments = client.getTreatmentsByFlagSet('frontend'); + +// Getting treatments for the union of multiple sets +const flagSets = ['frontend', 'client_side']; +treatments = client.getTreatmentsByFlagSets(flagSets); + +// treatments have the following form: +// { +// FEATURE_FLAG_NAME_1: 'on', +// FEATURE_FLAG_NAME_2: 'visa' +// } +``` + + + +### Get treatments with configurations + +To [leverage dynamic configurations with your treatments](https://help.split.io/hc/en-us/articles/360026943552), you should use the `getTreatmentWithConfig` method. + +This method will return an object with the structure below: + + + + +```javascript +var TreatmentResult = { + String treatment; + String config; // or null if there is no config for the treatment +} +``` + + +```javascript +type TreatmentResult = { + treatment: string, + config: string | null +}; +``` + + + +As you can see from the object structure, the config is a stringified version of the configuration JSON defined in the Split user interface. If there is no configuration defined for a treatment, the Suite returns `null` for the config parameter. This method takes the exact same set of arguments as the standard `getTreatment` method. See below for examples on proper usage: + + + + +```javascript +var treatmentResult = client.getTreatmentWithConfig('FEATURE_FLAG_NAME', attributes); +var configs = JSON.parse(treatmentResult.config); +var treatment = treatmentResult.treatment; + +if (treatment === 'on') { + // insert on code here and use configs here as necessary +} else if (treatment === 'off') { + // insert off code here and use configs here as necessary +} else { + // insert control code here +} +``` + + +```javascript +const treatmentResult: SplitIO.TreatmentWithConfig = client.getTreatmentWithConfig('FEATURE_FLAG_NAME', attributes); +const configs = JSON.parse(treatmentResult.config); +const treatment = treatmentResult.treatment; + +if (treatment === 'on') { + // insert on code here and use configs here as necessary +} else if (treatment === 'off') { + // insert off code here and use configs here as necessary +} else { + // insert control code here +} +``` + + + +If you need to get multiple evaluations at once, you can also use the `getTreatmentsWithConfig` methods. These methods take the exact same arguments as the [`getTreatments`](#multiple-evaluations-at-once) methods but return a mapping of feature flag names to TreatmentResults instead of strings. See example usage below: + + + + +```javascript +// Getting treatments by feature flag names +var flagNames = ['FEATURE_FLAG_NAME_1', 'FEATURE_FLAG_NAME_2']; +var treatmentResults = client.getTreatmentsWithConfig(flagNames); + +// Getting treatments by set +treatmentResults = client.getTreatmentsWithConfigByFlagSet('frontend'); + +// Getting treatments for the union of multiple sets +var flagSets = ['frontend', 'client_side']; +treatmentResults = client.getTreatmentsWithConfigByFlagSets(flagSets); + +// treatmentResults will have the following form: +// { +// FEATURE_FLAG_NAME_1: {treatment: 'on', +// config: "{ 'color' : 'red'}}", +// FEATURE_FLAG_NAME_2: {treatment: 'v2', +// config: "{ 'copy' : 'better copy'}}", +// } +``` + + +```javascript +// Getting treatments by feature flag names +const flagNames = ['FEATURE_FLAG_NAME_1', 'FEATURE_FLAG_NAME_2']; +let treatmentResults: SplitIO.TreatmentsWithConfig = client.getTreatmentsWithConfig(flagNames); + +// Getting treatments by set +treatmentResults = client.getTreatmentsWithConfigByFlagSet('frontend'); + +// Getting treatments for the union of multiple sets +const flagSets = ['frontend', 'client_side']; +treatmentResults = client.getTreatmentsWithConfigByFlagSets(flagSets); + +// treatmentResults will have the following form: +// { +// FEATURE_FLAG_NAME_1: {treatment: 'on', +// config: "{ 'color' : 'red'}}", +// FEATURE_FLAG_NAME_2: {treatment: 'v2', +// config: "{ 'copy' : 'better copy'}}", +// } +``` + + + +### Track + +Tracking **events** is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users' actions and metrics. See the [Events](https://help.split.io/hc/en-us/articles/360020585772) documentation for more information. + +The Suite automatically collects some RUM metrics and sends them to Split. Specifically, some [Performance API](https://developer.mozilla.org/en-US/docs/Web/API/Performance_API) events (see [Default events](https://help.split.io/hc/en-us/articles/360030898431-Browser-RUM-agent#default-events)) and Web Vitals events are automatically collected by the Suite. Learn more about these and other events in the [Browser RUM Agent](https://help.split.io/hc/en-us/articles/360030898431-Browser-RUM-agent#events) documentation. + +To track custom events, you can use the Suite client's `track` method or the Suite RUM agent's `track` method. Both methods are demonstrated in the code example below. + +The Suite client's `track` method sends events for the identity configured on the client instance or passed as a parameter to this method. The `track` method can take up to four arguments. The proper data type and syntax for each are: + +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: + * Contains 63 characters or fewer. + * Starts with a letter or number. + * Contains only letters, numbers, hyphen, underscore, or period. + * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` +* **VALUE:** (Optional) The value to be used in creating the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is **Integer** or **Float**. +* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. + +The Suite RUM agent's `track` method sends events for all the identities configured on all instances of the Suite clients. For those clients that have not been configured with a traffic type, the `track` method uses the default traffic type `user`. The Suite RUM agent's `track` method can take up to three of the four arguments described above: `EVENT_TYPE`, `VALUE`, and `PROPERTIES`. + + + +```javascript +var client = suite.client(); + +// The expected parameters are: +var queued = client.track('TRAFFIC_TYPE', 'EVENT_TYPE', eventValue, eventProperties); + +// Example with both a value and properties +var properties = { package : "premium", admin : true, discount : 50 }; +var queued = client.track('user', 'page_load_time', 83.334, properties); + +// Example with only properties +var properties = { package : "premium", admin : true, discount : 50 }; +var queued = client.track('user', 'page_load_time', null, properties); +``` + + +```javascript +var SplitRumAgent = suite.rumAgent(); + +// The expected parameters are: +var queued = SplitRumAgent.track('EVENT_TYPE', eventValue, eventProperties); + +// Example with both a value and properties +var properties = { package : "premium", admin : true, discount : 50 }; +var queued = SplitRumAgent.track('page_load_time', 83.334, properties); + +// Example with only properties +var properties = { package : "premium", admin : true, discount : 50 }; +var queued = SplitRumAgent.track('page_load_time', null, properties); +``` + + + +The `track` methods returns a boolean value of `true` or `false` to indicate whether or not the Suite was able to successfully queue the event to be sent back to Split's servers on the next event post. The `track` method returns `false` if the current queue size is equal to the config set by `eventsQueueSize` or if incorrect input has been provided. See the [Track events](https://help.split.io/hc/en-us/articles/360020585772-Track-events) documentation for more information. + +### Shutdown + +Split browser solutions are designed to evict pending impressions and events when a page is hidden or closed. Nonetheless you can call the `suite.destroy()` method before letting a process using the Suite exit. This method gracefully shuts down the Split Suite by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions and events. + + + + +```javascript +// You can just destroy and move on: +suite.destroy(); + +// destroy() returns a promise, so if you want to, for example, +// navigate to another page without losing impressions, you +// can do that once the promise resolves. +suite.destroy().then(function() { + document.location.replace('another_page'); +}); +``` + + + +After the `destroy` method is called and finishes, any subsequent invocations to `getTreatment`/`getTreatments` or manager methods result in `control` or an empty list, respectively. You can also call `destroy` on the client instance, which will stop the specific Suite client and remove this client's identity from the Suite's RUM agent, but will keep the Suite running. + +:::warning[Important!] +A call to the Suite's `destroy` method destroys all client objects and stops the Suite's RUM agent from tracking events. To create a new client instance, first create a new Suite instance. +::: + +## Configuration + +The Suite has a number of knobs for configuring performance. Each knob is tuned to a reasonable default. However, you can override the value while instantiating the Suite. The parameters available for configuration are shown below in separate tables for those parameters that affect feature flagging, those that affect the Suite RUM agent, and those that affect both. + +Feature flagging parameters: + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| core.labelsEnabled | Enable impression labels from being sent to Split cloud. Labels may contain sensitive information. | true | +| startup.readyTimeout | Maximum amount of time in seconds to wait before firing the `SDK_READY_TIMED_OUT` event | 10 | +| startup.requestTimeoutBeforeReady | The Suite has two main endpoints it uses /splitChanges and /mySegments that it hits to get ready. This config sets how long (in seconds) the Suite will wait for each request it makes as part of getting ready. | 5 | +| startup.retriesOnFailureBeforeReady | How many retries on /splitChanges and /mySegments we will do while getting the Suite ready | 1 | +| startup.eventsFirstPushWindow | Use to set a specific timer (expressed in seconds) for the first push of events, starting on Suite initialization. | 10 | +| scheduler.featuresRefreshRate | The Suite polls Split servers for changes to feature rollout plans. This parameter controls this polling period in seconds. | 60 | +| scheduler.segmentsRefreshRate | The Suite polls Split servers for changes to segment definitions. This parameter controls this polling period in seconds. | 60 | +| scheduler.impressionsRefreshRate | The Suite sends information on who got what treatment at what time back to Split servers to power analytics. This parameter controls how often this data is sent to Split servers. The parameter should be in seconds. | 300 | +| scheduler.impressionsQueueSize | The max amount of impressions we queue. If the queue is full, the Suite flushes the impressions and resets the timer. | 30000 | +| scheduler.eventsPushRate | The Suite sends tracked events to Split servers. This setting controls that flushing rate in seconds. | 60 | +| scheduler.eventsQueueSize | The max amount of events we queue. If the queue is full, the Suite flushes the events and resets the timer. | 500 | +| scheduler.telemetryRefreshRate | The Suite caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers (in seconds). | 3600 seconds (1 hour) | +| sync.splitFilters | Filter specific feature flags to be synced and evaluated by the Suite. This is formed by a type string property and a list of string values for the given criteria. Using the types 'bySet' (recommended, flag sets are available in all tiers) or 'byName', pass an array of strings defining the query. If empty or unset, all feature flags are downloaded by the Suite. | [] | +| sync.impressionsMode | This configuration defines how impressions (decisioning events) are queued on the Suite. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Split; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, ALL impressions are queued and sent to Split; this is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. | `OPTIMIZED` | +| sync.enabled | Controls the Suite continuous synchronization flags. When `true`, a running Suite processes rollout plan updates performed in the Split user interface (default). When `false`, it fetches all data upon init, which ensures a consistent experience during a user session and optimizes resources when these updates are not consumed by the app. | true | +| sync.requestOptions.getHeaderOverrides | A callback function that can be used to override the Authentication header or append new headers to the Suite's HTTP(S) requests. | undefined | +| storage | Pluggable storage instance to be used by the Suite as a complement to in memory storage. Only supported option today is `InLocalStorage`. See the [Configuration](#configuring-localstorage-cache-for-the-sdk) section for details. | In memory storage | +| streamingEnabled | Boolean flag to enable the streaming service as default synchronization mechanism. In the event of an issue with streaming, the Suite will fallback to the polling mechanism. If false, the Suite will poll for changes as usual without attempting to use streaming. | true | + +Suite RUM agent parameters: + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| rumAgent.prefix | Optional prefix to append to the `eventTypeId` of the events sent to Split by the RUM Agent. For example, if you set the prefix to 'my-app', the event type 'error' will be sent as 'my-app.error'. | undefined | +| rumAgent.pushRate | The Agent posts the queued events data in bulks. This parameter controls the posting rate in seconds. | 30 | +| rumAgent.queueSize | The maximum number of event items the RUM Agent will queue. If more values are queued, events will be dropped until they are sent to Split. | 5000 | +| rumAgent.eventCollectors | The RUM Agent tracks some events by default using event collectors. These event collectors include errors, navigation timing metrics (`page.load.time` and `time.to.dom.interactive` event types), and Web-Vitals. You can disable any of them by setting their value to `false`. Go to [RUM Agent events](https://help.split.io/hc/en-us/articles/360030898431-Browser-RUM-Agent#events) for more information on each event. | \{ errors: true, navigationTiming: true, webVitals: true \} | + +Shared parameters: + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| debug | Either a boolean flag, string log level or logger instance for activating logging. See the [Logging](#logging) section for details. | false | +| userConsent | User consent status used to control the tracking of events and impressions. Possible values are `GRANTED`, `DECLINED`, and `UNKNOWN`. See the [User consent](#user-consent) section for details. | `GRANTED` | + +To set each of the parameters defined above, use the following syntax: + + + +```javascript +var suite = SplitSuite({ + startup: { + readyTimeout: 10, // 10 sec + eventsFirstPushWindow: 10 // 10 sec + }, + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'YOUR_KEY' + }, + scheduler: { + featuresRefreshRate: 5, // 5 sec + segmentsRefreshRate: 60, // 60 sec + impressionsRefreshRate: 300, // 300 sec + impressionsQueueSize: 30000, // 30000 items + eventsPushRate: 60, // 60 sec + eventsQueueSize: 500, // 500 items + telemetryRefreshRate: 3600 // 1 hour + }, + sync: { + splitFilters: [{ + type: 'bySet', + values: ['frontend'] + }], + impressionsMode: 'OPTIMIZED' + }, + streamingEnabled: true, + debug: false, + rumAgent: { + prefix: 'my-app', + pushRate: 30, // 30 sec + queueSize: 5000, + eventCollectors: { + errors: true, + navigationTiming: true, + webVitals: true + } + } +}); +``` + + +```javascript +const suite: ISuiteSDK = SplitSuite({ + startup: { + readyTimeout: 10, // 10 sec + eventsFirstPushWindow: 10 // 10 sec + }, + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'YOUR_KEY' + }, + scheduler: { + featuresRefreshRate: 5, // 5 sec + segmentsRefreshRate: 60, // 60 sec + impressionsRefreshRate: 300, // 300 sec + impressionsQueueSize: 30000, // 30000 items + eventsPushRate: 60, // 60 sec + eventsQueueSize: 500, // 500 items + telemetryRefreshRate: 3600 // 1 hour + }, + sync: { + splitFilters: [{ + type: 'bySet', + values: ['frontend'] + }], + impressionsMode: 'OPTIMIZED' + }, + streamingEnabled: true, + debug: false, + rumAgent: { + prefix: 'my-app', + pushRate: 30, // 30 sec + queueSize: 5000, + eventCollectors: { + errors: true, + navigationTiming: true, + webVitals: true + } + } +}); +``` + + + +### Configuring LocalStorage cache for the Suite + +To use the pluggable `InLocalStorage` option of the Suite and be able to cache flags for subsequent loads in the same browser, you need to pass it to the Suite config as the `storage` option. + +This `InLocalStorage` function accepts an optional object with options described below: + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| prefix | An optional prefix for your data, to avoid collisions. | `SPLITIO` | + + + +```javascript +import { SplitSuite, InLocalStorage } from '@splitsoftware/browser-suite'; + +const suite = SplitSuite({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + storage: InLocalStorage({ + prefix: 'MY_PREFIX' + }) +}); + +// Now use the Suite as usual +const client = suite.client(); +``` + + + +## Localhost mode + +For testing, a developer can evaluate Split feature flags on their development machine without requiring network connectivity. To achieve this, the Suite can be started in **localhost** mode (aka off-the-grid or offline mode). In this mode, the Suite neither polls nor updates Split servers. Instead, it uses an in-memory data structure to determine the treatment returned by any given feature flag. + +Define the feature flags you want to use in the `features` object map. All `getTreatment` calls for a feature flag now only return the one treatment (and config, if defined) that you have defined in the map. You can then change the treatment as necessary for your testing. To update a treatment or a config, or to add or remove feature flags from the mock cache, update the properties of the `features` object you've provided. The SDK simulates polling for changes and updates from it. Do not assign a new object to the `features` property because the SDK has a reference to the original object and will not detect the change. + +Any feature flag that is not provided in the `features` map returns the [control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment) if the SDK is asked to evaluate it. + +You can use the additional configuration parameters below when instantiating the SDK in `localhost` mode. + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| scheduler.offlineRefreshRate | The refresh interval for the mocked features treatments. | 15 | +| features | A fixed mapping of which treatment to show for our mocked features. | {}
By default we have no mocked features. | + +To use the SDK in localhost mode, replace the SDK key on `authorizationKey` property with `'localhost'`, as shown in the example below. Note that you can define in the `features` object a feature flag name and its treatment directly or use a map to define both a treatment and a dynamic configuration. + +If you define just a string as the value for a feature flag name, any config returned by our SDKs are always null. If you use a map, we return the specified treatment and the specified config (which can also be null). + + + +```javascript +import { SplitSuite } from '@splitsoftware/browser-suite'; + +const suite = SplitSuite({ + core: { + authorizationKey: 'localhost' + }, + features: { + 'reporting_v2': 'on', // example with just a string value for the treatment + 'billing_updates': { treatment: 'visa', config: '{ "color": "blue"}' } // example of a defined config + 'show_status_bar': { treatment: 'off', config: null } // example of a null config + }, + scheduler: { + offlineRefreshRate: 15 // 15 sec + }, +}); + +const client = suite.client(); + +// The following code will be evaluated once the engine finishes the initialization +client.on(client.Event.SDK_READY, () => { + // The sentence below will return 'on' + const t1 = client.getTreatment('reporting_v2'); + // The sentence below will return an object with the structure of: {treatment:'visa',config:'{ "color":"blue" }' + const t2 = client.getTreatmentWithConfig('billing_updates'); + // The sentence below will return 'control' because that feature does not exist + const t3 = client.getTreatmentWithConfig('navigation_bar_changes'); +}); +``` + + + +## Manager + +Use the Split Manager to get a list of features available to the Split client. To instantiate a Manager in your code base, use the same suite instance that you used for your client: + + + + +```javascript +var suite = SplitSuite({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + // the key can be the logged in + // user id, or the account id that + // the logged in user belongs to. + // The type of customer (user, account, custom) + // is chosen during Split's sign-up process. + key: 'key' + } +}); + +var manager = suite.manager(); + +manager.once(manager.Event.SDK_READY, function() { + // Once it's ready, use the manager +}); +``` + + +```javascript +const suite: ISuiteSDK = SplitSuite({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + // the key can be the logged in + // user id, or the account id that + // the logged in user belongs to. + // The type of customer (user, account, custom) + // is chosen during Split's sign-up process. + key: 'key' + } +}); + +const manager: SplitIO.IManager = suite.manager(); + +manager.once(manager.Event.SDK_READY, function() { + // Once it's ready, use the manager +}); +``` + + + +The Manager has the following methods available: + + + +```javascript +/** + * Returns the feature flag registered within the SDK that matches this name. + * + * @return SplitView or null. + */ +var splitView = manager.split('name-of-feature-flag'); + +/** + * Retrieves all the feature flags that are currently registered within the SDK. + * + * returns a List of SplitViews. + */ +var splitViewsList = manager.splits(); + +/** + * Returns the names of all feature flags registered within the SDK. + * + * @return a List of Strings of the features' names. + */ +var splitNamesList = manager.names(); +``` + + +```javascript +/** + * Returns the feature flag registered within the SDK that matches this name. + * + * @return SplitView or null. + */ +const splitView: SplitIO.SplitView = manager.split('name-of-feature-flag'); + +/** + * Retrieves all the feature flags that are currently registered within the SDK. + * + * returns a List of SplitViews. + */ +const splitViewsList: SplitIO.SplitViews = manager.splits(); + +/** + * Returns the names of all feature flags registered within the SDK. + * + * @return a List of Strings of the features' names. + */ +const splitNamesList: SplitIO.SplitNames = manager.names(); +``` + + + +The `SplitView` object referenced above has the following structure: + + + +```typescript +type SplitView = { + name: string, + trafficType: string, + killed: boolean, + treatments: Array, + changeNumber: number, + configs: { + [treatmentName: string]: string + }, + defaultTreatment: string, + sets: Array +} +``` + + + +## Listener + +The Split Suite sends impression data back to Split servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression listener*. For that purpose, the Suite's configurations have a parameter called `impressionListener` where an implementation of `ImpressionListener` could be added. This implementation **must** define the `logImpression` method and it receives data in the following schema. + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| impression | Object | Impression object that has the feature, key, treatment, label, etc. | +| attributes | Object | A map of attributes passed to `getTreatment`/`getTreatments` (if any). | +| sdkLanguageVersion | String| The version of the SDK. In this case the language is `browserjs` plus the version currently running. | + +:::info[Note] +There are two additional keys on this object, `ip` and `hostname`. They are not captured but kept for consistency with server-side SDKs. +::: + +### Implement custom impression listener + +The following is an example of how to implement a custom impression listener. + + + + +```javascript +function logImpression(impressionData) { + // do something with the impression data. +} + +var suite = SplitSuite({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + impressionListener: { + logImpression: logImpression + } +}); +``` + + +```javascript +class MyImprListener implements SplitIO.IImpressionListener { + logImpression(impressionData: SplitIO.ImpressionData) { + // do something with impressionData + } +} + +const suite: ISuiteSDK = SplitSuite({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + impressionListener: { + logImpression: new MyImprListener() + } +}); +``` + + + +An impression listener is called asynchronously from the corresponding evaluation, but is almost immediate. + +Even though the SDK does not fail if there is an exception in the listener, do not block the call stack. + +## Logging + +To trim as many bits as possible from the user application builds, we divided the logger in implementations that contain the log messages for each log level: `ErrorLogger`, `WarnLogger`, `InfoLogger`, and `DebugLogger`. Higher log level options contain the messages for the lower ones, with DebugLogger containing them all. To enable descriptive logging, you need to plug in a logger instance as shown below: + + + +```javascript +import { SplitSuite, DebugLogger } from '@splitsoftware/browser-suite'; + +const suite = SplitSuite({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + debug: DebugLogger() // other options are `InfoLogger`, `WarnLogger` and `ErrorLogger` +}); +``` + + +```javascript +var splitio = require('@splitsoftware/browser-suite'); + +var suite = splitio.SplitSuite({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + debug: splitio.DebugLogger() // other options are `InfoLogger`, `WarnLogger` and `ErrorLogger` +}); +``` + + + +You can also enable the Suite logging via a boolean or log level value as `debug` settings, and change it dynamically by calling the Suite Logger API. However, in any case where the proper logger instance is not plugged in, instead of a human readable message, you'll get a code and optionally some params for the log itself. If you find yourself in a scenario where you need to parse this information, you can check the constant files in our javascript-commons repository (where you have tags per version if needed) under the [logger folder](https://github.com/splitio/javascript-commons/blob/master/src/logger/). + +```javascript title="Logger API" +import { SplitSuite } from '@splitsoftware/browser-suite'; + +const suite = SplitSuite({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + debug: true // other options are 'ERROR', 'WARN', 'INFO' and 'DEBUG +}); + +// Or you can use the Logger API methods which have an immediate effect. +suite.Logger.setLogLevel('WARN'); // Acceptable values are: 'DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE' +suite.Logger.enable(); // equivalent to `setLogLevel('DEBUG')` +suite.Logger.disable(); // equivalent to `setLogLevel('NONE')` +``` + +Suite logging can also be globally enabled via a localStorage value by opening your DevTools console and typing the following: + +```javascript title="Enable logging from browser console" +// Acceptable values are 'DEBUG', 'INFO', 'WARN', 'ERROR' and 'NONE' +// Other acceptable values are 'on', 'enable' and 'enabled', which are equivalent to 'DEBUG' log level +localStorage.splitio_debug = 'on' +``` + +## Advanced use cases + +This section describes advanced use cases and features provided by the Suite. + +### Instantiate multiple clients + +Split supports the ability to release based on multiple traffic types. For example, with traffic types, you can release to `users` in one feature flag and `accounts` in another. If you are unfamiliar with using multiple traffic types, refer to the [Traffic type guide](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) for more information. + +Each Suite client is tied to one specific customer ID at a time, so if you need to roll out feature flags by different traffic types, instantiate multiple clients, one for each traffic type. For example, you may want to roll out the feature `user-poll` by `users` and the feature `account-permissioning` by `accounts`. + +You can do this with the example below: + + + + +```javascript +var suite = SplitSuite({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'CUSTOMER_ACCOUNT_ID', + // Instantiate the suite once and provide the ID for one of the + // traffic types that you plan to release to. It doesn't + // matter which you pick to start off with. + }, +}); + +// now when you call suite.client(), the suite creates a client +// using the Account ID and traffic type name (if any) +// you passed in during the suite creation. +var account_client = suite.client(); + +// to create another client for a User instead, just pass in a User ID + +// This is only valid after at least one client has been initialized. +var user_client = suite.client('CUSTOMER_USER_ID'); + +// check treatment for user-poll and CUSTOMER_USER_ID +var user_poll_treatment = user_client.getTreatment('user-poll'); + +// check treatment for account-permissioning and CUSTOMER_ACCOUNT_ID +var account_permissioning_treatment = account_client.getTreatment('account-permissioning'); + +// track events for accounts +user_client.track('account', 'PAGELOAD', 7.86); + +// or track events for users +account_client.track('user', 'ACCOUNT_CREATED'); +``` + + +```javascript +const suite: ISuiteSDK = SplitSuite({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'CUSTOMER_ACCOUNT_ID' + // instantiate the suite once and provide the ID for one of the + // traffic types that you plan to release to. It doesn't + // matter which you pick to start off with. + }, +}); + +// now when you call suite.client(), the suite will create a client +// using the Account ID you passed in during the suite creation. +const account_client: SplitIO.IClient = suite.client(); + +// to create another client for a User instead, just pass in a +// User ID to the suite.client() method. This is only valid after +// at least one client has been initialized. +const user_client: SplitIO.IClient = suite.client('CUSTOMER_USER_ID'); + +// check treatment for user-poll and CUSTOMER_USER_ID +const user_poll_treatment: SplitIO.Treatment = + user_client.getTreatment('user-poll'); + +// check treatment for account-permissioning and CUSTOMER_ACCOUNT_ID +const account_permissioning_treatment: SplitIO.Treatment = + account_client.getTreatment('account-permissioning'); + +// track events for accounts +user_client.track('account', 'PAGELOAD', 7.86); + +// or track events for users +account_client.track('user', 'ACCOUNT_CREATED'); +``` + + + +The events captured by the RUM Agent are sent to Split servers using the traffic types and keys of the created client. If no traffic type is provided, the traffic type is `user` by default. + +:::info[Number of Suite instances] +While the Suite does not put any limitations on the number of instances that can be created, we strongly recommend keeping the number of instances down to **one** or **two**. +::: + +### Subscribe to events + +You can listen for four different events from the Suite. + +* `SDK_READY_FROM_CACHE`. This event fires once the Suite is ready to evaluate treatments using a version of your rollout plan cached in localStorage from a previous session (which might be stale). If there is data in localStorage, this event fires almost immediately, since access to localStorage is fast; otherwise, it doesn't fire. +* `SDK_READY`. This event fires once the Suite is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Split servers. +* `SDK_READY_TIMED_OUT`. This event fires if there is no cached version of your rollout plan cached in localStorage, and the Suite could not download the data from Split servers within the time specified by the `readyTimeout` configuration parameter. This event does not indicate that the Suite initialization was interrupted. The Suite continues downloading the rollout plan and fires the `SDK_READY` event when finished. This delayed `SDK_READY` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. +* `SDK_UPDATE`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in the Split user interface. + +The syntax to listen for each event is shown below: + + + + +```javascript +function whenReady() { + var treatment = client.getTreatment('YOUR_SPLIT'); + if (treatment === 'on') { + // insert on code + } else if (treatment === 'off') { + // insert off code + } else { + // insert control code (usually the same as default treatment) + } +} +client.once(client.Event.SDK_READY, function () { + // the client is ready to evaluate treatments according to the latest feature flag definitions +}); + +client.once(client.Event.SDK_READY_TIMED_OUT, function () { + // this callback will be called after the set timeout period has elapsed if and only if the client + // is not ready for that time. You can still call getTreatment() + // but it could return CONTROL. +}); + +client.on(client.Event.SDK_UPDATE, function () { + // fired each time the client state changes. + // For example, when a feature flag or a segment changes. + console.log('Feature flag or segment definitions have been updated!'); +}); + +// This event fires only using the LocalStorage option and if there's Split data stored in the browser. +client.once(client.Event.SDK_READY_FROM_CACHE, function () { + // Fired after the Suite could confirm the presence of the Split data. + // This event fires really quickly, since there's no actual fetching of information. + // Keep in mind that data might be stale, this is NOT a replacement of SDK_READY. +}); +``` + + +```javascript +function whenReady() { + const treatment: SplitIO.Treatment = client.getTreatment('YOUR_SPLIT'); + + if (treatment === 'on') { + // insert on code + } else if (treatment === 'off') { + // insert off code + } else { + // insert control code (usually the same as default treatment) + } +} + +// the client is ready for start making evaluations with your data +client.once(client.Event.SDK_READY, whenReady); + +client.once(client.Event.SDK_READY_TIMED_OUT, () => { + // this callback will be called after 1.5 seconds if and only if the client + // is not ready for that time. You can still call getTreatment() + // but it could return CONTROL. +}); + +client.on(client.Event.SDK_UPDATE, () => { + // fired each time the client state change. + // For example, when a feature flag or a segment changes. + console.log('Feature flag or segment definitions have been updated!'); +}); + +// This event fires only using the LocalStorage option and if there's Split data stored in the browser. +client.once(client.Event.SDK_READY_FROM_CACHE, function () { + // Fired after the Suite could confirm the presence of the Split data. + // This event fires really quickly, since there's no actual fetching of information. + // Keep in mind that data might be stale, this is NOT a replacement of SDK_READY. +}); +``` + + + +### User consent + +The Suite allows you to disable the tracking of events and impressions until user consent is explicitly granted or declined. + +The `userConsent` configuration parameter lets you set the initial consent status of the Suite instance, and the Suite method `UserConsent.setStatus(boolean)` lets you grant (enable) or decline (disable) the dynamic data tracking. + +There are three possible initial states: + * `'GRANTED'`: the user grants consent for tracking events and impressions. The Suite sends them to Split cloud. This is the default value if `userConsent` param is not defined. + * `'DECLINED'`: the user declines consent for tracking events and impressions. The Suite does not send them to Split cloud. + * `'UNKNOWN'`: the user neither grants nor declines consent for tracking events and impressions. The Suite tracks them in its internal storage, and eventually either sends them or not if the consent status is updated to `'GRANTED'` or `'DECLINED'` respectively. The status can be updated at any time with the `UserConsent.setStatus` Suite method. + + + +```javascript +var suite = SplitSuite({ + core: { + authorizationKey: 'YOUR_SDK_KEY', + key: 'key' + }, + // Overwrites the initial consent status of the suite instance, which is 'GRANTED' by default. + // 'UNKNOWN' status represents that the user has neither granted nor declined consent for tracking data, + // so the suite will locally track data but not send it to Split cloud until consent is changed to 'GRANTED'. + userConsent: 'UNKNOWN' +}); + + +// `getStatus` method returns the current consent status. +suite.UserConsent.getStatus() === suite.UserConsent.Status.UNKNOWN; + +// `setStatus` method lets you update the suite consent status at any moment. +// Pass `true` for 'GRANTED' and `false` for 'DECLINED'. +suite.UserConsent.setStatus(true); // Consent status changed from 'UNKNOWN' to 'GRANTED'. Data will be sent to Split cloud. +suite.UserConsent.getStatus() === suite.UserConsent.Status.GRANTED; + +suite.UserConsent.setStatus(false); // Consent status changed from 'GRANTED' to 'DECLINED'. Data will not be sent to Split cloud. +suite.UserConsent.getStatus() === suite.UserConsent.Status.DECLINED; + +``` + + + +### RUM agent configuration + +The Suite handles the setup of the RUM agent using the same SDK key. Configurations for [Logging](#logging), [User consent](#user-consent) and [Identities](#instantiate-multiple-clients) are also shared between the Suite and the RUM agent. + +You can further configure the RUM agent using the `rumAgent` property of the Suite configuration object, which is passed as the second argument to the RUM agent's [`setup` method](https://help.split.io/hc/en-us/articles/360030898431-Browser-RUM-agent#configuration): + +```javascript +import { SplitSuite } from '@splitsoftware/browser-suite'; + +const suite = SplitSuite({ + ... + rumAgent: { + // Optional prefix to append to the `eventTypeId` of the events sent to Split. + // For example, if you set the prefix to 'my-app', the event type 'error' will be sent as 'my-app.error'. + prefix: 'my-app', + // The agent posts the queued events data in bulks. This parameter controls the posting rate in seconds. + pushRate: 30, + // The maximum number of event items we want to queue. If we queue more values, events will be dropped until they are sent to Split. + queueSize: 5000, + } +}) +``` + +To extend the RUM agent with custom event collectors or properties, you can use the `SplitRumAgent` singleton instance, retrieving it with the Suite's `rumAgent` method, as shown below: + +```javascript +import { SplitSuite, SplitRumAgent, routeChanges } from '@splitsoftware/browser-suite'; + +SplitRumAgent.register(routeChanges()); + +const suite = SplitSuite(config); + +suite.rumAgent().setProperties({ 'application': 'application_name' }); +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/ios-suite.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/ios-suite.md new file mode 100644 index 00000000000..aa4f0a6b9a9 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/ios-suite.md @@ -0,0 +1,853 @@ +--- +title: iOS Suite +sidebar_label: iOS Suite +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +This guide provides detailed information about our iOS Suite, an SDK designed to harness the full power of Split. The iOS Suite is built on top of the [iOS SDK](https://help.split.io/hc/en-us/articles/360020401491-iOS-SDK) and the [iOS RUM Agent](https://help.split.io/hc/en-us/articles/22545155055373-iOS-RUM-Agent), offering a unified solution, optimized for iOS development. + +The Suite provides the all-encompassing essential programming interface for working with your Split feature flags, as well as capabilities for automatically tracking performance measurements and user events. Code currently using iOS SDK or iOS RUM Agent can be easily upgraded to iOS Suite, which is designed as a drop-in replacement. + +## Language support + +This library is designed for iOS applications written in Swift and is compatible with iOS versions 12 and later. + +## Initialization + +Set up Split in your code base with the following two steps: + +### 1. Import the Suite into your project + +Add the Split SDK, Split RUM agent, and Split Suite into your project using Swift Package Manager by adding the following package dependencies: + +- [iOS SDK] (https://github.com/splitio/ios-client), latest version `3.0.0` +- [iOS RUM](https://github.com/splitio/ios-rum), latest version `0.4.0` +- [iOS Suite](https://github.com/splitio/ios-suite), latest version `2.0.1` + +:::info[Important!] +When not using the last version of the Split Suite, it is important to take into account the compatibility matrix below. +::: + +| Suite | SDK | RUM | +|----------|----------|----------| +| 1.0.0 | 2.24.6 | 0.3.0 | +| 1.1.0 | 2.24.7 | 0.4.0 | +| 1.2.0 | 2.25.0 | 0.4.0 | +| 1.3.0 | 2.26.1 | 0.4.0 | +| 2.0.0 | 3.0.0 | 0.4.0 | +| 2.0.1 | 3.0.0 | 0.4.0 | + +Then import the Suite in your code. + +```swift title="Swift" +import iOSSplitSuite +``` + +### 2. Instantiate the Suite and create a new Split client + +In your code, instantiate the Suite client as shown below. + +```swift title="Swift" +// Create default Suite configuration +let config = SplitSuiteConfig() + +// Split SDK key +let sdkKey = "YOUR_SDK_KEY" +let matchingKey = Key(matchingKey: "key") + +// Create Suite +let suite = SplitSuite.builder() + .apiKey(sdkKey) + .key(matchingKey) + .config(config).build() + +// Get Split Client instance +let client = suite?.client; +``` + +:::info[Important] +If you are upgrading from Split's iOS RUM Agent to iOS Suite and you have setup or config information for the iOS RUM Agent in the `SplitRumAgent-Info.plist`, then this information will be overridden by the Suite initialization. That is why we recommended that you remove this information from that file when upgrading. +::: + +When the Suite is instantiated, it starts synchronizing feature flag and segment definitions from Split servers, and also starts collecting performance and user events for the configured key and its optional traffic type (which if not set, defaults to `'user'`). + +We recommend instantiating the Suite once as a singleton and reusing it throughout your application. + +Configure the Suite with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split UI, on your Admin settings page, API keys section. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. + +## Using the Suite + +### Basic use + +When the Suite is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds depending on the size of the data. If the Suite is asked to evaluate which treatment to show to a user for a specific feature flag while in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the Suite does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). + +To make sure the Suite is properly loaded before asking it for a treatment, block until the Suite is ready, as shown below. We set the client to listen for the `SDK_READY` event triggered by the Suite before asking for an evaluation. + +After the `SDK_READY` event fires, you can use the `getTreatment` method to return the proper treatment based on the `FEATURE_FLAG_NAME` and the `key` variables you passed when instantiating the Suite. + +You can use an if-else statement as shown below and insert the code for the different treatments that you defined in the Split user interface. Remember to handle the client returning control, for example, in the final else statement. + +```swift title="Swift" +client?.on(event: SplitEvent.sdkReady) { + // Evaluate feature flag in Split + let treatment = client?.getTreatment("FEATURE_FLAG_NAME") + + if treatment == "on" { + // insert code here to show on treatment + } else if treatment == "off" { + // insert code here to show off treatment + } else { + // insert your control treatment code here + } +} +``` + +### Attribute syntax + +To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the Suite's `getTreatment` method needs to be passed an attribute map at runtime. + +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in the Split user interface to decide whether to show the `on` or `off` treatment to this account. + +The `getTreatment` method has a number of variations that are described below. Each of these additionally has a variation that takes an attributes argument, which can defines attributes of the following types: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: + +* **Strings:** Use type `String`. +* **Numbers:** Use type `Int64`. +* **Dates:** Use the value `TimeInterval`. For instance, the value for the `registered_date` attribute below is `Date().timeIntervalSince1970`, which is a `TimeInterval` value. +* **Booleans:** Use type `Bool`. +* **Sets:** Use type `[String]`. + +```swift title="Swift" +var attributes: [String:Any] = [:] + +attributes["plan_type"] = "growth" +attributes["registered_date"] = Date().timeIntervalSince1970 +attributes["deal_size"] = 1000 +attributes["paying_customer"] = true +let perms: [String] = ["read", "write"]; +attributes["permissions"] = perms + +// See client initialization above +let treatment = client?.getTreatment("FEATURE_FLAG_NAME", attributes: attributes) + +if treatment == "on" { + // insert code here to show on treatment +} else if treatment == "off" { + // insert code here to show off treatment +} else { + // insert your control treatment code here +} +``` + +You can pass your attributes in exactly this way to the `client.getTreatments` method. + +### Binding attributes to the client + +Attributes can be bound to the client at any time during the Suite lifecycle. These attributes will be stored in memory and used in every evaluation to avoid the need for keeping the attribute set accessible through the whole app. These attributes can be cached into the persistent caching mechanism of the Suite making them available for future sessions, as well as part of the SDK_READY_FROM_CACHE flow by setting the `persistentAttributesEnabled` to true. No need to wait for your attributes to be loaded at every session before evaluating flags that use them. + +When an evaluation is called, the attributes provided (if any) at evaluation time are combined with the ones already loaded into the Suite memory, with the ones provided at function execution time take precedence, enabling for those attributes to be overridden or hidden for specific evaluations. + +An attribute is considered valid if it follows one of the types listed below: +- String +- Number +- Boolean +- Array + +The Suite validates these before storing them and if there are invalid or missing values, possibly indicating an issue, the methods return the boolean `false` and do not update any value. + +The snippet below shows how to update these attributes: + +```swift title="Swift" +// Prepare a Map with several attributes +var attributes: [String:Any] = [:] +attributes["plan_type"] = "growth" +attributes["registered_date"] = Date().timeIntervalSince1970 +attributes["deal_size"] = 1000 + +// Now set these on the client +let result = client.setAttributes(attributes) + +// Set one attribute +let result = client.setAttribute(name: "registered_date", value: Date().timeIntervalSince1970) + +// Get an attribute +let result = client.getAttribute(name: "registered_date") + +// Get all attributes +let result = client.getAttributes() + +// Remove an attribute +let result = client.removeAttribute(name: "deal_size") + +// Remove all attributes +let result = client.clearAttributes() +``` + +### Multiple evaluations at once + +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` method of the Split client to do this. +* `getTreatments`: Pass a list of the feature flag names you want treatments for. +* `getTreatmentsByFlagSet`: Evaluate all flags that are part of the provided set name and are cached on the Suite instance. +* `getTreatmentsByFlagSets`: Evaluate all flags that are part of the provided set names and are cached on the Suite instance. + +```swift title="Swift" +// Assuming client is an instance of a class that has these methods +let featureFlagNames = ["FEATURE_FLAG_NAME_1", "FEATURE_FLAG_NAME_2"] +let treatments = client.getTreatments(splits: featureFlagNames, attributes: nil) + +let treatmentsByFlagSet = client.getTreatmentsByFlagSet("frontend", attributes: nil) + +let flagSets = ["frontend", "client_side"] +let treatmentsByFlagSets = client.getTreatmentsByFlagSets(flagSets, attributes: nil) + +// Treatments will have the following form: +// [ +// "FEATURE_FLAG_NAME_1": "on", +// "FEATURE_FLAG_NAME_2": "visa" +// ] + +// Treatments will have the following form: +// [ +// "FEATURE_FLAG_NAME_1": "on", +// "FEATURE_FLAG_NAME_2": "visa" +// ] +``` + +### Get treatments with configurations + +To [leverage dynamic configurations with your treatments](https://help.split.io/hc/en-us/articles/360026943552), use the `getTreatmentWithConfig` method. This method returns an object containing the treatment and associated configuration. + +The config element is a stringified version of the configuration JSON defined in the Split user interface. If there is no configuration defined for a treatment, the `result.config` property will be `nil`. + +This method takes the exact same set of arguments as the standard `getTreatment` method. See below for examples on proper usage: + +```swift title="Swift" +let result = client.getTreatmentWithConfig("new_boxes", attributes: attributes) +let config = try? JSONSerialization.jsonObject(with: result.config.data(using: .utf8)!, options: []) as? [String: Any] +let treatment = result.treatment +``` +If you need to get multiple evaluations at once, you can also use `getTreatmentsWithConfig` methods. These methods take the exact same arguments as the [`getTreatments`](#multiple-evaluations-at-once) methods but return a mapping of feature flag names to results instead of strings. Refer to the example below. + +```swift title="Swift" +let featureFlagList = ["FEATURE_FLAG_NAME_1", "FEATURE_FLAG_NAME_2", "FEATURE_FLAG_NAME_3"] +let treatments = client?.getTreatmentsWithConfig(splits: featureFlagList, attributes: nil) + +let treatmentsByFlagSet = client.getTreatmentsWithConfigByFlagSet("frontend", attributes: nil) + +let flagSets = ["frontend", "client_side"] +let treatmentsByFlagSets = client.getTreatmentsWithConfigByFlagSets(flagSets, attributes: nil) + +// treatments will have the following form: +// { +// "FEATURE_FLAG_NAME_1": { "treatment": "on", "config": "{ \"color\":\"red\" }"}, +// "FEATURE_FLAG_NAME_2": { "treatment": "visa", "config": "{ \"color\":\"red\" }"} +// } +``` + +### Track + +Tracking events is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users' actions and metrics. See the [Events](https://help.split.io/hc/en-us/articles/360020585772) documentation for more information. + +The Suite automatically collects some RUM metrics and sends them to Split. Specifically, crashes, ANRs and app start time (see [Default events](https://help.split.io/hc/en-us/articles/22545155055373-iOS-RUM-Agent#default-events-and-properties)) are automatically collected by the Suite. Learn more about these and other events in the [iOS RUM Agent](https://help.split.io/hc/en-us/articles/22545155055373-iOS-RUM-Agent#events) documentation. + +To track custom events, you can use the `client.track()` method or the `suite.track()` method. Both methods are demonstrated in the code examples below. + +The `client.track()` method sends events **_for the identity configured on the client instance_**. This `track` method can take up to four arguments. The proper data type and syntax for each are: + +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is `String`. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in the Split UI. +* **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is `String`. Full requirements on this argument are: + * Contains 63 characters or fewer. + * Starts with a letter or number. + * Contains only letters, numbers, hyphen, underscore, or period. + * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}`. +* **VALUE:** (Optional) The value used in creating the metric. This field can be sent in as nil or 0 if you intend to only use the count function when creating a metric. The expected data type is `Double`. +* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. + +The `suite.track()` method sends events **_for all the identities_** configured on all instances of the Suite clients. For those clients that have not been configured with a traffic type, this `track` method uses the default traffic type `user`. This `track` method can take up to three of the four arguments described above: `EVENT_TYPE`, `VALUE`, and `PROPERTIES`. + +Tracking per identity using `client.track()`: + +```swift title="Swift" +let client = factory.client + +// Expected parameteres are +let resp = client.track(trafficType: "TRAFFIC_TYPE", eventType: "EVENT-TYPE", , value: VALUE, properties: PROPERTIES) + +// Example with both a value and properties +let properties: [String:Any] = ["package": "premium", "discount": 50, "admin": true] +let resp = client?.track(trafficType: "user", eventType: "page_load_time", value: 83.334, properties: properties)) + +// Example with only properties +let properties: [String:Any] = ["package": "premium", "discount": 50, "admin": true] +let resp = client?.track(trafficType: "user", eventType: "EVENT-TYPE", properties: properties) + +``` + +Tracking for all identities using `suite.track()`: + +```swift title="Swift" +// If you would like to send an event but you've already defined the traffic type in the config of the suite +let resp = suite.track(eventType: "EVENT-TYPE", value: nil, properties: nil) +// Example +let resp = suite.track(eventType: "page_load_time", value: nil, properties: nil) + +// If you would like to associate a value to an event and you've already defined the traffic type in the config of the suite +let resp = suite.track(eventType: "EVENT-TYPE", value: VALUE, properties: nil) +// Example +let resp = suite.track(eventType: "page_load_time", value: 83.334, properties: nil) + +// If you would like to associate properties to an event and you've already defined the traffic type in the config of the suite +let resp = suite.track(eventType: "EVENT-TYPE", properties: PROPERTIES) +// Example +let properties: [String:Any] = ["package": "premium", "discount": 50, "admin": true] +let resp = suite.track(eventType: "page_load_time", proerties: properties) + +``` + +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event sent back to Split's servers on the next event post. The SDK returns `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method is provided. + +### Shutdown + +Before letting your app shut down, call `destroy()` as it gracefully shuts down the Suite by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions and events. + +```swift title="Swift" +client?.destroy() +``` + +Also, this method has a completion closure which can be used to run some code after destroy is executed. For instance, the following snippet waits until destroy has finished to continue execution: +```swift title="Swift" +let semaphore = DispatchSemaphore(value: 0) +client?.destroy(completion: { + _ = semaphore.signal() +}) +semaphore.wait() +``` + +After `destroy()` is called, any subsequent invocations to the `client.getTreatment()` or `manager` methods result in `control` or an empty list respectively. + +:::warning[Important!] +A call to the `destroy()` method also destroys the factory object. When creating new client instance, first create a new factory instance. +::: + +## Configuration + +The Suite has a number of settings for configuring performance, and each setting is set to a reasonable default. You can override the settings when instantiating the Suite. The available configuration settings are shown below in separate tables for those settings that affect feature flagging, those that affect the Suite RUM agent, and those that affect both. + +Feature flagging parameters: + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| featuresRefreshRate | The Suite polls Split servers for changes to feature flags at this rate (in seconds). | 3600 seconds (1 hour) | +| segmentsRefreshRate | The Suite polls Split servers for changes to segments at this rate (in seconds). | 1800 seconds (30 minutes) | +| impressionRefreshRate | The treatment log captures which customer saw what treatment (on, off, etc.) at what time. This log is periodically flushed back to Split servers. This configuration controls how quickly the cache expires after a write (in seconds). | 1800 seconds (30 minutes) | +| impressionsQueueSize | Default queue size for impressions. | 30K | +| eventsPushRate | When using `.track`, how often the events queue is flushed to Split servers. | 1800 seconds| +| eventsPerPush | Maximum size of the batch to push events. | 2000 | +| eventsFirstPushWindow | Amount of time to wait for the first flush. | 10 seconds | +| eventsQueueSize | When using `.track`, the number of **events** to be kept in memory. | 10000 | +| trafficType | (optional) The default traffic type for events tracked using the `track` method. If not specified, every `track` call should specify a traffic type. | not set | +| telemetryRefreshRate | The Suite caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers (in seconds). | 3600 seconds (1 hour) | +| logLevel | Enables logging according to the level specified. Options are `NONE`, `VERBOSE`, `DEBUG`, `INFO`, `WARNING`, and `ERROR`. | `NONE` | +| synchronizeInBackground | Activates synchronization when application host is in background. | `false` | +| streamingEnabled | Boolean flag to enable the streaming service as default synchronization mechanism when in foreground. In the event of an issue with streaming, the Suite falls back to the polling mechanism. If false, the Suite polls for changes as usual without attempting to use streaming. | `true` | +| sync | Optional SyncConfig instance. Use it to filter specific feature flags to be synced and evaluated by the Suite. These filters can be created with the `SplitFilter::bySet` static function (recommended, flag sets are available in all tiers), or `SplitFilter::byName` static function, and appended to this config using the `SyncConfig` builder. If not set or empty, all feature flags are downloaded by the Suite. | `nil` | +| offlineRefreshRate | The Suite periodically reloads the localhost mocked feature flags at this given rate in seconds. This can be turned off by setting it to -1 instead of a positive number. | -1 (off) | +| sdkReadyTimeOut | Amount of time in milliseconds to wait before notifying a timeout. | -1 (not set) | +| persistentAttributesEnabled | Enables saving attributes on persistent cache which is loaded as part of the SDK_READY_FROM_CACHE flow. All functions that mutate the stored attributes map affect the persistent cache.| `false` | +| syncEnabled | Controls the Suite continuous synchronization flags. When `true`, a running Suite processes rollout plan updates performed in the Split user interface (default). When `false`, it fetches all data upon init, which ensures a consistent experience during a user session and optimizes resources when these updates are not consumed by the app. | `true` | +| userConsent | User consent status used to control the tracking of events and impressions. Possible values are `GRANTED`, `DECLINED`, and `UNKNOWN`. See the [User consent](#user-consent) section for details. | `GRANTED` | +| encryptionEnabled | Enables or disables encryption for cached data. | `false` | +| httpsAuthenticator | If set, the Suite uses it to authenticate network requests. To set this value, an implementation of SplitHttpAuthenticator must be provided. | `nil` | +| prefix | Allows to use a prefix when naming the Suite storage. Use this when using multiple `SplitFactory` instances with the same SDK key. | `nil` | +| certificatePinningConfig | If set, enables certificate pinning for the given domains. For details, see the [Certificate pinning](#certificate-pinning) section below. | null | + +Suite RUM agent parameters: + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| prefix | Optional prefix to append to the `eventTypeId` of the events sent to Split by the Suite RUM agent. For example, if you set the prefix to 'my-app', the event type 'error' will be sent as 'my-app.error'. | `nil` | + +Shared parameters: + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| logLevel | Enables logging according to the level specified. Options are `NONE`, `VERBOSE`, `DEBUG`, `INFO`, `WARNING`, `ERROR`, and `ASSERT`. | `NONE` | + +To set each of the parameters defined above, use the following syntax: +```swift title="Swift" +import Split + +// Your Split SDK key +let sdkKey: String = "YOUR_SDK_KEY" + +//User Key +let key: Key = Key(matchingKey: "key") + +//Split Configuration +let config = SplitClientConfig() +config.impressionRefreshRate = 30 +config.isDebugModeEnabled = false +let syncConfig = SyncConfig.builder() + .addSplitFilter(SplitFilter.bySet(["frontend"])) + .build() +config.sync = syncConfig + +//Split Factory +let builder = DefaultSplitFactoryBuilder() +let factory = +builder.setApiKey(sdkKey).setKey(key).setConfig(config).build() + +//Split Client +let client = factory?.client +``` + +## Localhost mode + +For testing, a developer can put code behind feature flags on their development machine without the Suite requiring network connectivity. To achieve this, you can start the Suite in **localhost** mode (aka, off-the-grid mode). In this mode, the Suite neither polls or updates Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the feature flags. To use the Suite in localhost mode, replace the SDK Key with `localhost`, as shown in the example below. + +The format for defining the definitions is as follows: + +```yaml title="YAML" +- my_feature: + treatment: "on" + keys: "key" + config: "{\"desc\" : \"this applies only to ON treatment\"}" +- some_other_feature: + treatment: "off" +- my_feature: + treatment: "off" +- other_feature: + treatment: "off" + keys: ["key_1", "key_2"] + config: "{\"desc\" : \"this overrides multiple keys and returns off treatment for those keys\"}" +``` + +In the example above, we have four entries: + + * The first entry defines that for feature flag `my_feature`, the key `key` returns the treatment `on` and the `on` treatment is tied to the configuration `{"desc" : "this applies only to ON treatment"}`. + * The second entry defines that the feature flag `some_other_feature` always returns the `off` treatment and no configuration. + * The third entry defines that `my_feature` always returns `off` for all keys that don't match another entry (in this case, any key other than `key`). + * The fourth entry shows an example on how to override a treatment for a set of keys. + +In this mode, the Split Suite loads the yaml file from a resource bundle file at the assets' project `src/main/assets/splits.yaml`. + +```swift title="Swift" +// Split SDK key must be "localhost" +let apiKey: String = "localhost" +let key: Key = Key(matchingKey: "key") +let config = SplitClientConfig() +config.splitFile = "localhost.yaml" +let builder = DefaultSplitFactoryBuilder() +self.factory = +builder.setApiKey("localhost").setKey(key).setConfig(config).build() +``` + +If `SplitClientConfig.splitFile` is not set, the Suite maintains backward compatibility by trying to load the legacy file (.splits), now deprecated. In this mode, the Suite loads a local file called *localhost.splits* which has the following line format: + +FEATURE_FLAG_NAME TREATMENT + +You can update feature flag definitions programmatically by using the `updateLocalhost` method, as shown below. + +```swift title="Swift" +// Split SDK key must be "localhost" +let apiKey: String = "localhost" +let key: Key = Key(matchingKey: "key") +let config = SplitClientConfig() +let builder = DefaultSplitFactoryBuilder() +self.factory = builder.setApiKey("localhost").setKey(key).setConfig(config).build() + +// SplitLocalhostDataSource protocol declares the updating methods +if guard let datasource = self.factory as? SplitLocalhostDataSource else { return } + +// Yalm file content +datasource.updateLocalhost(yaml: yaml_content) + +// Split file content +datasource.updateLocalhost(splits: splits_content) +``` + +Additionally, you can include comments in the file by starting a line with the ## character. + +A sample *localhost.splits* file: + +```bash title="Shell" + ## This line is a comment + ## Following line has feature flag = FEATURE_ONE and treatment = ON + FEATURE_ONE ON + FEATURE_TWO OFF + ## Previous line has feature flag = FEATURE_TWO, treatment = OFF +``` + +By enabling debug mode, the *localhost* file location is logged to the console, so it's possible to open the file in a text editor when working on the simulator. When using the device to run the app, the file can be modified by overwriting the app's bundle from the **Device and Simulators** tool. + +## Manager + +Use the Split Manager to get a list of feature flags available to the Split client. + +To instantiate a Manager in your code base, use the same factory that you used for your client. + +```swift title="Swift" +let apiKey: String = "YOUR_API_KEY" +let key: Key = Key(matchingKey: "key") +let config = SplitClientConfig() +let builder = DefaultSplitFactoryBuilder() +let factory = +builder.setApiKey(apiKey).setKey(key).setConfig(config).build() +let manager = factory?.manager +``` + +The Manager then has the following properties and methods available. + +```swift title="Swift" +/** + * Retrieves the feature flags that are currently registered with the + * Suite. + * + * @return an array of SplitView or empty. + */ +var splits: [SplitView] { get } + +/** + * Returns the names of feature flags registered with the Suite. + * + * @return an array of String (feature flag names) or empty + */ +var splitNames: [String] { get } + +/** + * Returns the feature flag registered with the Suite of this name. + * + * @return SplitView or nil + */ +func split(featureName: String) -> SplitView? +``` + +The `SplitView` class referenced above has the following structure. + +```swift title="Swift" +public class SplitView: NSObject, Codable { + + @objc public var name: String? + @objc public var trafficType: String? + @objc public var defaultTreatment: String? + public var killed: Bool? + @objc public var isKilled: Bool { + return killed ?? false + } + @objc public var treatments: [String]? + @objc public var sets: [String]? + public var changeNumber: Int64? + + @objc public var changeNum: NSNumber? { + return changeNumber as NSNumber? + } + @objc public var configs: [String: String]? + +} +``` + +## Listener + +Split Suite sends impression data back to Split servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression handler*. + +The Suite sends the generated impressions to the impression handler right away. As a result, be careful while implementing handling logic to avoid blocking the main thread. Generally speaking, you should create a separate thread to handle incoming impressions. Refer to the snippet below. + +```swift title="Swift" +let config = SplitClientConfig() +config.impressionListener = { impression in + // Do some work on main thread + DispatchQueue.global().async { + // Do some async work (use this most of the time!) + } +} + +let key: Key = Key(matchingKey: "key") +let builder = DefaultSplitFactoryBuilder() +let factory = +builder.setApiKey(apiKey).setKey(key).setConfig(config).build() +let client = factory?.client +``` + +In regards with the data available here, refer to the `impression` objects interface and description of each field below. + +```swift title="Swift" + feature: String? + keyName: String? + treatment: String? + time: Int64? + changeNumber: Int64? + label: String? + bucketingKey: String? + attributes: [String: Any]? +``` + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| keyName | String? | Key which is evaluated. | +| bucketingKey | String? | Key which is used for bucketing, if provided. | +| feature | String? | Feature flag which is evaluated. | +| treatment | String? | Treatment that is returned. | +| time | Int64? | Timestamp of when the impression is generated. | +| label | String? | Targeting rule in the definition that matched resulting in the treatment being returned. | +| changeNumber | Int64? | Date and time of the last change to the targeting rule that the Suite used when it served the treatment. This can be used to help understand when a change made to a feature flag got picked up by the Suite, and whether one of the Suite instances is not picking up changes. | +| attributes | [String: Any]? | A map of attributes passed to `getTreatment`/`getTreatments`, if any. | + +## Flush + +The flush() method sends the data stored in memory (impressions and events) to Split cloud and clears the successfully posted data. If a connection issue is experienced, the data will be sent on the next attempt. + +```swift title="Swift" +client.flush() +``` + +## Background synchronization + +Background synchronization is available for devices having iOS 13+. +To enable this feature, just follow the next 4 steps: + +1. Enable _[Background Mode Fetch](https://developer.apple.com/documentation/watchkit/background_execution/enabling_background_sessions)_ capability for your app. +2. Add the Split Suite background sync task identifier *io.split.bg-sync.task* to the Permitted background task scheduler identifiers section of the Info.plist . +3. Set the Split config flag _synchronizeInBackground_ to true . + +```swift title="Swift" +let config = SplitClientConfig() +config.synchronizeInBackground = true + +... +``` + +4. Schedule the background sync during app startup. e.g., _application(\_:didFinishLaunchingWithOptions:)_ + +```swift title="Swift" +SplitBgSynchronizer.shared.schedule() + +... +``` + +:::warning[Important!] +Due to an iOS limitation for background fetch capability, only one task identifier is allowed for that purpose. If there is already a current background fetch identifier registered, background sync may not work. +::: + +## Logging + +To enable logging, the `logLevel` setting is available in `SplitClientConfig` class: + +```swift title="Setup logs" +// This setting type is `SplitLogLevel`. +// The available values are .verbose, .debug, .info, .warning, .error and .none + let config = SplitClientConfig() + config.logLevel = .verbose + + ... +``` + +The following shows example output: + +

+ ios_log_example.png +

+ +## Advanced use cases + +This section describes advanced use cases and features provided by the Suite. + +### Instantiate multiple Suite clients + +Split Suite supports the ability to create multiple clients, one for each user ID. For example, if you need to roll out feature flags for different user IDs, you can instantiate multiple clients, one for each ID. You can then evaluate flags for each ID using the corresponding client. You can do this as shown in the example below: + +```swift title="Swift" +// Create factory +let key = Key(matchingKey: "anonymous_user") +let config = SplitClientConfig() +let factory = DefaultSplitFactoryBuilder().setApiKey(authorizationKey) + .setKey(key) + .setConfig(config).build() + +// Now when you call factory.client, the Suite will create a client +// using the anonymous_user key +// you passed in during the factory creation +let anonymousClient = factory.client + +// To create another client for a user instead, pass in a User ID +let userClient = factory.client(matchingKey: "user_id") + +// Add events handler for each client to be notified when Suite is ready +anonymousClient.on(event: SplitEvent.sdkReady, execute: { + // anonymousClient is ready to evaluate + // Check treatment for anonymous users + let accountPermissioningTreatment = anonymousClient.getTreatment("some_feature_flag") +}) + +userClient.on(event: SplitEvent.sdkReady, execute: { + // userClient is ready to evaluate + // Check treatment for the feature flag and user_id + let userPollTreatment = userClient.getTreatment("some_feature_flag") +}) +``` + +:::info[Number of Suite instances] +While the Suite does not put any limitations on the number of `SplitFactory` instances that you can create, we strongly recommend keeping the number of Suite factory instances down to **one** or **two**. +::: + +### Subscribe to events + +You can listen for four different events from the Suite. + +* `sdkReadyFromCache`: This event fires once the Suite is ready to evaluate treatments using a locally cached version of your rollout plan from a previous session (which might be stale). If there is data in the cache, this event fires almost immediately, since access to the cache is fast; otherwise, it doesn't fire. +* ` sdkReady`: This event fires once the Suite is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Split servers. +* ` sdkReadyTimedOut`: This event fires if there is no cached version of your rollout plan in disk cache, and the Suite could not fully download the data from Split servers within the time specified by the `sdkReadyTimeOut` property of the `SplitClientConfig` object. This event does not indicate that the Suite initialization was interrupted. The Suite continues downloading the rollout plan and fires the `sdkReady` event when finished. This delayed `sdkReady` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. +* `sdkUpdated`: This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in the Split user interface. + +Split Suite event handling is done through the `on(event:execute:)` function, which receives a closure as an event handler. The code within the closure is executed on the main thread. For that reason, running code in the background must be done explicitly. + +The syntax to listen for an event is shown below. + +```swift title="Swift" +... +let client = factory.client + +client.on(event: SplitEvent.sdkReady, execute: { + // The client is ready to evaluate treatments according to the latest feature flag definitions + // Do some stuff on main thread +}) + +// Or +client.on(event: SplitEvent.sdkReadyTimedOut) { + // This callback will be called if and only if the client is configured with ready timeout and + // is not ready for that time or if the API key is wrong. + // You can still call getTreatment() but it could return CONTROL. + + // Do some stuff on main thread +} + +client.on(event: SplitEvent.sdkReadyFromCache) { + // Fired after the Suite confirms the presence of the Split data. + // This event fires quickly, since there's no actual fetching of information. + // Keep in mind that data might be stale, this is NOT a replacement of sdkReady. + + // Do some stuff on main thread +} + +client.on(event: SplitEvent.sdkUpdated) { + // fired each time the client state change. + // For example, when a feature flag or segment changes. + + // Do some stuff on main thread +} + +... +``` + +### RUM agent configuration + +The Suite handles the setup of its RUM agent using the same SDK key. Configurations for [Logging](#logging) and [Identities](#instantiate-multiple-clients) are also shared with the Suite's RUM agent. + +You can further configure the RUM agent by passing a `SplitSuiteConfig` object instead of `SplitClientConfig`. + +```swift title="Swift" +// Optionally create feature flagging configuration +let sdkConfig = SplitClientConfig() +sdkConfig.logLevel = .verbose +sdkConfig.trafficType = "custom" + +// Specify a prefix for the Suite events +let suiteConfig = SplitSuiteConfig().prefix("new.v7") + +// Instantiate the Suite passing in the SplitSuiteConfiguration +let suite = DefaultSplitSuite + .builder() + .setApiKey("YOUR_SDK_KEY") + .setKey(Key(matchingKey: "key")) + .setConfig(sdkConfig) // This methods is overloaded + .setConfig(suiteConfig).build() +``` + +### User consent + +By default the Suite will send events to Split cloud, but you can disable this behavior until user consent is explicitly granted. + +The `userConsent` configuration parameter lets you set the initial consent status of the Suite, and the `suite.setUserConsent(boolean)` method lets you grant (enable) or decline (disable) dynamic event tracking. + +There are three possible initial states: + * `'GRANTED'`: The user grants consent for tracking events and impressions. The SDK sends them to Split cloud. This is the default value if `userConsent` param is not defined. + * `'DECLINED'`: The user declines consent for tracking events and impressions. The SDK does not send them to Split cloud. + * `'UNKNOWN'`: The user neither grants nor declines consent for tracking events and impressions. The SDK tracks them in its internal storage, and eventually either sends them or not if the consent status is updated to `'GRANTED'` or `'DECLINED'` respectively. + +The status can be updated at any time with the `setUserConsent` factory method. + +Working with user consent is demonstrated below. + +```swift title="User consent: Initial config, getter and setter" + // Overwrites the initial consent status of the Suite instance, which is 'GRANTED' by default. + // 'UNKNOWN' status represents that the user has neither granted nor declined consent for tracking data, + // so the Suite locally tracks data but not send it to Split cloud until consent is changed to 'GRANTED'. + let sdkConfig = SplitClientConfig() + sdkConfig.userConsent = .unknown + + // Split SDK key + let sdkKey = "YOUR_SDK_KEY" + let matchingKey = Key(matchingKey: "key") + + // Create Suite + let suite = SplitSuite.builder() + .apiKey(sdkKey) + .key(matchingKey) + .config(sdkConfig).build() + + // Changed User Consent status to 'GRANTED'. Data will be sent to Split cloud. + suite.setUserConsent(enabled: true); + // Changed User Consent status to 'DECLINED'. Data will not be sent to Split cloud. + suite.setUserConsent(enabled: false); + + // The 'getUserConsent' method returns User Consent status. + // We expose the constants for customer checks and tracking. + if (suite.userConsent == UserConsent.declined) { + print("USER CONSENT DECLINED"); + } + if (suite.userConsent == UserConsent.granted) { + print("USER CONSENT GRANTED"); + } + if (suite.userConsent == UserConsent.unknown) { + print("USER CONSENT UNKNOWN"); + } +``` + +### Certificate pinning + +The SDK allows you to constrain the certificates that the SDK trusts, using one of the following techniques: + +1. Pin a certificate's `SubjectPublicKeyInfo`, by providing the public key as a ___base64 SHA-256___ hash or a ___base64 SHA-1___ hash. +2. Pin a certificate's entire certificate chain (the root, all intermediate, and the leaf certificate), by providing the certificate chain as a .der file. + +Each pin corresponds to a host. For subdomains, you can optionally use wildcards, where `*` will match one subdomain (e.g. `*.example.com`), and `**` will match any number of subdomains (e.g `**.example.com`). + +You can optionally configure a handler to execute on certificate validation failure for a host. + +To set the SDK to require pinned certificates for specific hosts, add the `CertificatePinningConfig` object to `SplitClientConfig`, as shown below. + +```swift title="Swift" +// Define pins for certificate pinning +let certBuilder = CertificatePinningConfig.builder() + +// Provide a base 64 SHA-256 hash +certBuilder.addPin(host: "www.example1.com", hashKey: "sha256/7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=") + +// Provide a certificate file name. This file has to be added to the bundle. +certBuilder.addPin(host: "www.example2.com", certificateName: "certificate.der") + +// Set a failure handler +certBuilder.certificatePinningConfig { host in + print("Failed validation for host \(host)") +} + +// Set the CertificatePinningConfig property for the Split client configuration +let config = SplitClientConfig() +config.certificatePinningConfig = certBuilder.build() +// you can add other configuration properties here + +... +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/_category_.json b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/_category_.json new file mode 100644 index 00000000000..f6a181e1597 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "FAQs: Client-side SDKs", + "collapsible": "true", + "collapsed": "true", + "className": "red", + "position": 11 +} \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/_category_.json b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/_category_.json new file mode 100644 index 00000000000..bcbe5c8da90 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "FAQs: General SDK", + "collapsible": "true", + "collapsed": "true", + "className": "red", + "position": 10 +} \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/_category_.json b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/_category_.json new file mode 100644 index 00000000000..293b8480998 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "FAQs: Optional infrastructure", + "collapsible": "true", + "collapsed": "true", + "className": "red", + "position": 8 +} \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/_category_.json b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/_category_.json new file mode 100644 index 00000000000..cfa0b35d503 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "FAQs: Server-side SDKs", + "collapsible": "true", + "collapsed": "true", + "className": "red", + "position": 12 +} \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/_category_.json b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/_category_.json new file mode 100644 index 00000000000..98e96ca2e51 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Optional infrastructure", + "collapsible": "true", + "collapsed": "true", + "className": "red", + "position": 8 +} \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-daemon-splitd.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-daemon-splitd.md new file mode 100644 index 00000000000..91215790af1 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-daemon-splitd.md @@ -0,0 +1,378 @@ +--- +title: Split Daemon (splitd) +sidebar_label: Split Daemon (splitd) +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +Splitd is a daemon that communicates with the Split backend. It keeps an up-to-date snapshot of the Split rollout plan for a specific Split environment. The rollout plan is accessed by a Split Thin SDK instance (via splitd) to consume feature flags in your code. + +Splitd can be used if you are working in a language that does not have native capability to keep a shared local cache, such as PHP. You can use splitd in combination with a Split Thin SDK (see [Supported Thin SDKs](#supported-thin-sdks)) as an alternative to using Split Synchronizer and Redis with a non-thin Split SDK. Splitd easily scales to high traffic volumes. + +Splitd is a daemon designed to be deployed in the same host as the application, and works as an offloaded evaluation engine running on a separate process. Splitd relies on the host machine's memory to store the cache and offers an IPC interface via Unix sockets (both stream and sequenced packet sockets supported). Consumer applications (that hold Split Thin SDK instances) connect to splitd and send evaluation requests for Split feature flags as remote procedure calls. Impressions are generated on the daemon and can be backfed to the client to trigger an impression listener. + +## Supported Thin SDKs + +Splitd currently works with the following Split Thin SDKs: + +* [Elixir Thin Client SDK](https://help.split.io/hc/en-us/articles/26988707417869) +* [PHP Thin Client SDK](https://help.split.io/hc/en-us/articles/18305128673933) + +If you are looking for a language that is not listed here, contact the support team at [support@split.io](mailto:support@split.io). + +## Overall Architecture + +The service performs three actions: + +* **Fetch targeting rules:** Fetch feature flags and segments from the Split servers. +* **Perform evaluations:** Split Thin SDKs will establish a connection to splitd and send evaluation requests that splitd will service. +* **Post impressions and events:** Impressions (data about a customer's feature flag evaluations) and events will be temporarily stored in the daemon's memory and periodically sent to Split servers. + +### Architecture + +

+splitd_arch.drawio.svg +

+ +## Setup + +Splitd can be set up locally to the consumer application or be deployed as a sidecar to the consumer application container. These approaches are described below. + +### Local deployment (recommended) + +The following diagram illustrates a local setup where splitd is on the same server instance as the consumer application. It shows how splitd communicates with the application via IPC/Unix socket connections. + +

+splitd_shared_instance.drawio.svg +

+ +Since the service relies on interprocess communication (IPC) with thin clients, which happens on the operating system's kernel, the two processes need to be on the same host. The most straightforward way to achieve this is by having both the daemon and the application that bundles the Split Thin SDK in the same server/instance/container. + +To set up splitd on the same host as the Split Thin SDK, follow the steps below: + +#### 1. Get a copy of splitd + +You can get a copy of splitd using one of the following options: + +* Github release: +`wget https://github.com/splitio/splitd/releases/download/v1.4.0/splitd---1.4.0.bin` where OS in [linux, darwin] and ARCH in [amd64, arm] + +* `go install` command (requires a Go development environment): +`go install github.com/splitio/splitd/cmd/splitd@v1.4.0` + +* Build from source (requires a Go development environment and GNU Make): +`git clone github.com/splitio/splitd && cd splitd && make splitd` + +#### 2. Create a configuration file + +Using [splitd.yaml.tpl](https://github.com/splitio/splitd/blob/main/splitd.yaml.tpl) as a template, create your own configuration with at least a valid API key and a path for the IPC socket where both the daemon and the application have read-write access. + +If you have the `yq` command installed, you can get an initial config with the following command: + +```bash +export SDK_KEY="your_apikey" +wget https://raw.githubusercontent.com/splitio/splitd/main/splitd.yaml.tpl \ + -O /dev/stdout 2> /dev/null \ + | yq '.sdk.apikey = env(SDK_KEY)' > splitd.yaml +``` + +:::warning[OSX & Unix sockets] +Keep in mind that seqpacket-type sockets only work on the Linux operating system. If you're running a proof of concept on a Mac, you need to set the link type to `unix-stream` in both the daemon and the Split SDK configurations. For more information on socket types, see the [Advanced configuration](#advanced-configuration) section. +::: + +:::info[Configuration file location] +By default, splitd searches for the configuration file at `/etc/splitd.yaml`. This behaviour can be overridden by setting the `SPLITD_CONF_FILE` environment variable. +::: + +#### 3. Running splitd + +To start splitd, execute the binary `splitd`. This will find and parse the splitd configuration file and start the splitd daemon. Applications using a Split Thin SDK should now be able to connect to the socket created by splitd. + +When integrating splitd with an application on a server, you will want the daemon to run at system startup and be managed by a background process. The process should automatically restart splitd if it is killed (for example, by the kernel’s memory management). Two popular options are deploying the splitd as a systemd unit or as a supervisord program. These approaches are described below. + +##### a. Deploying splitd as a systemd unit + +Below is an example of a unit file with configuration directives that can be used to deploy splitd as a systemd unit. + +```ini +[Unit] +Description=Split daemon for feature flagging +After=syslog.target network-online.target remote-fs.target nss-lookup.target +Wants=network-online.target + +[Service] +Type=simple +User=splitd +Group=http +UMask=002 +Environment=SPLITD_CONF_FILE="/opt/splitd/splitd.yaml" +PIDFile=/run/splitd.pid +ExecStart=/opt/bin/splitd +ExecStop=/bin/kill -s QUIT $MAINPID +PrivateTmp=true + +[Install] +WantedBy=multi-user.target +``` + +##### b. Deploying splitd as a supervisord program + +[Supervisord](http://supervisord.org/) is a client/server system to monitor and control a number of processes on UNIX-like operating systems. Below is an example configuration for supervisord to manage splitd. + +```ini +[group:splitd] +programs=splitd +priority=20 + +[program:splitd] +user = splitd +umask = 002 +command = /opt/splitd/splitd +process_name=%(program_name)s +startsecs = 0 +autostart = true +autorestart = true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +environment = SPLITD_CONF_FILE="/opt/splitd/splitd.yaml" +``` + +This example configuration instructs supervisord to use a UNIX user account with the name 'splitd' to run the opt/splitd/splitd executable. You’ll need to create this UNIX user account before running this script, and run supervisord as the root user. See the [supervisord documentation](http://supervisord.org/) for the full Configuration File spec. + +:::warning[Socket & permissions] +Special care should be taken to make the folder containing the IPC socket files read-writable by both splitd and the consumer application, as well as setting a umask parameter (so that sockets created in the folder inherit appropriate permissions). +::: + +### Containerized sidecar + +Since communication happens at a kernel level, containers running on the same host are able to connect via IPC. This means you can have a multi-container setup where splitd and the consumer application live in separate containers and use a shared mount volume to access the IPC socket handle. For this purpose we provide a sidecar-ready image which can be downloaded and easily integrated into an existing Docker Compose or Kubernetes infrastructure. + +The following image illustrates architecture when splitd is running in a sidecar container. + +

+splitd_sidecar.drawio.svg +

+ +#### 1. Pull the image + +You can pull the image using the following command: + +`docker pull splitsoftware/splitd-sidecar:latest` + +#### 2. Setup a multi-container infrastructure + +A multi-container infrastructure can be set up using Docker or Kubernetes. Both approaches are outlined below. + +##### a. Setup splitd using Docker Compose + +Below is an example of Docker Compose running splitd as a sidecar for a consumer application, using a mount volume to store the IPC socket, accessible by both the daemon and the application. + +```yaml +version: "3.9" + +services: + + splitd-sidecar: + image: splitsoftware/splitd-sidecar + volumes: + - ${HOME}/uds:/shared + environment: + SPLITD_APIKEY: "MY_APIKEY" + SPLITD_LINK_ADDRESS: "/shared/splitd.sock" + + application: + depends_on: + - "splitd-sidecar" + image: organization/application:latest + volumes: + - ${HOME}/uds:/shared + environment: + SPLIT_IPC_SOCKET_ADDRESS: "/shared/splitd.sock" + ports: + - 80 +``` + +:::warning[splitd configuration environment variables] +For a more comprehensive list of environment variables accepted by the splitd daemon, see the [Advanced Configuration](#advanced-configuration) section. +::: + +##### b. Set up splitd using Kubernetes + +Below is an example of a Kubernetes deployment file, using the sidecar pattern to attach an extra container to an application pod. The socket is made available by splitd to the application using a shared mount volume between both containers. + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + name: application-main +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + name: application-main + template: + metadata: + labels: + name: application-main + spec: + volumes: + - name: splitd-app-shared-volume + emptyDir: {} + containers: + - name: app + image: someOrg/application-main + volumeMounts: + - mountPath: "/shared" + name: splitd-app-shared-volume + readOnly: false + env: + - name: SPLITD_LINK_ADDRESS + value: "/shared/splitd.sock" + - name: SPLITD_LINK_TYPE + value: "unix-seqpacket" + ports: + - containerPort: 80 + name: app + resources: + requests: + cpu: "200m" + memory: "128Mi" + limits: + cpu: "1000m" + memory: "512Mi" + startupProbe: + tcpSocket: + port: app + initialDelaySeconds: 25 + timeoutSeconds: 5 + periodSeconds: 5 + successThreshold: 1 + failureThreshold: 3 + livenessProbe: + tcpSocket: + port: app + timeoutSeconds: 5 + periodSeconds: 5 + successThreshold: 1 + failureThreshold: 3 + + - name: splitd + image: splitsoftware/splitd-sidecar + volumeMounts: + - mountPath: "/shared" + name: splitd-app-shared-volume + readOnly: false + env: + - name: SPLITD_APIKEY + valueFrom: + secretKeyRef: + name: app-main + key: SPLIT_APIKEY + value: "https://streaming.split.io/sse" + - name: SPLITD_LINK_ADDRESS + value: "/shared/splitd.sock" + - name: SPLITD_LINK_TYPE + value: "unix-seqpacket" + resources: + requests: + cpu: "200m" + memory: "128Mi" + limits: + cpu: "1000m" + memory: "512Mi" + imagePullSecrets: + - name: docker-registry + securityContext: + fsGroup: 65534 +``` + +## Advanced configuration + +The daemon has several configuration options that can be tuned to make the deployment work in different situations. We offer a template [splitd.yaml.tpl](https://raw.githubusercontent.com/splitio/splitd/main/splitd.yaml.tpl) that can be used as a starting point with reasonable defaults for most use cases. It can then be tailored to suit more specific needs such as supporting larger payloads, higher concurrency, etc. + +:::info[Configuration for container-based environments] +For container-based deployment, our image uses an entrypoint.sh shell executable file that captures environment variables and uses them to generate a yaml file at startup, before executing the daemon. +::: + +:::info[Configuration for local installations] +For non-container based deployments, you must ensure that both the splitd binary and the final yaml configuration are placed on the server. The location of the configuration file defaults to `/etc/splitd.yaml` and can be specified by the `SPLITD_CONF_FILE` environment variable. +::: + +```yaml title="splitd.yaml" +logging: + level: error + output: /dev/stdout +sdk: + apikey: + impressions: + mode: optimized + queueSize: 8192 + events: + queueSize: 8192 +link: + type: unix-seqpacket + address: /var/run/splitd.sock + bufferSize: 1024 +``` + +### YAML Configuration options and its equivalents in environment variables + +| **YAML option** | **Environment variable (container-only)** | **Description** | **Default** | **Since** | +| --- | --- | --- | --- | --- | +| sdk.apikey | SPLITD_APIKEY | **(required always)** Server-side Split API key | EMPTY | 1.0.0 | +| sdk.streamingEnabled | SPLITD_STREAMING_ENABLED | Whether to enable streaming | `true` | 1.0.0 | +| sdk.labelsEnabled | SPLITD_LABELS_ENABLED | Whether to send labels on impressions | `true` | 1.0.0 | +| sdk.featureFlags.splitRefreshSeconds | SPLITD_FEATURE_FLAGS_SPLIT_REFRESH_SECS | Refresh rate when operating in polling (seconds) | `30` | 1.0.1 | +| sdk.featureFlags.segmentRefreshSeconds | SPLITD_FEATURE_FLAGS_SEGMENT_REFRESH_SECS | Refresh rate for segments when operating in polling (seconds) | `60` | 1.0.1 | +| sdk.impressions.mode | SPLITD_IMPRESSIONS_MODE | Impressions handling strategy [`optimized`/`debug`] | `optimized` | 1.0.1 | +| sdk.impressions.refreshRateSeconds | SPLITD_IMPRESSIONS_REFRESH_SECS | How often to flush impressions to Split servers | 1800 | 1.0.1 | +| sdk.impressions.queueSize | SPLITD_IMPRESSIONS_QUEUE_SIZE | How many impressions (per client) to accumulate before flushing | `8192` | 1.0.1 | +| sdk.events.refreshRateSeconds | SPLITD_EVENTS_REFRESH_SECS | How often to flush events to Split servers | `60` | 1.0.1 | +| sdk.events.queueSize | SPLITD_EVENTS_QUEUE_SIZE | How many events (per client) to accumulate before flushing | `8192` | 1.0.1 | +| sdk.flagSetsFilter | SPLITD_FLAG_SETS_FILTER | This setting allows the Split Synchronizer to synchronize only the feature flags in the specified flag sets. All other flags are not synchronized, resulting in a reduced payload. | empty | 1.2.0 | +| sdk.urls.auth | SPLITD_AUTH_URL | Auth Endpoint | `https://auth.split.io/` | 1.0.0 | +| sdk.urls.sdk |SPLITD_SDK_URL | SDK Targeting Rules Endpoint | `https://sdk.split.io/api` | 1.0.0 | +| sdk.urls.events | SPLITD_EVENTS_URL | Events and Impressions Endpoint | `https://events.split.io/api` | 1.0.0 | +| sdk.urls.streaming | SPLITD_TELEMETRY_URL | Telemetry Endpoint | `https://telemetry.split.io/api/v1`| 1.0.0 | +| sdk.urls.telemetry | SPLITD_STREAMING_URL | Streaming Endpoint | `https://streaming.split.io/sse` | 1.0.0 | +| link.type | SPLITD_LINK_TYPE | Type of socket to use [`unix-stream`/`unix-seqpacket`] | `unix-seqpacket` | 1.0.0 | +| link.serialization | SPLITD_LINK_SERIALIZATION | Serialization mechanism to use. Only messagepack currently supported | `msgpack` | 1.0.0 +| link.maxSimultaneousConns | SPLITD_LINK_MAX_CONNS | Maximum number of clients that can be attached at the same time | `1024` | 1.0.0 +| link.readTimeoutMS | SPLITD_LINK_READ_TIMEOUT_MS | Socket read timeout in milliseconds | `1000` | 1.0.0 | +| link.writeTimeoutMS |SPLITD_LINK_WRITE_TIMEOUT_MS | Socket write timeout in milliseconds | `1000` | 1.0.0 | +| link.acceptTimeoutMS | SPLITD_LINK_ACCEPT_TIMEOUT_MS | Socket accept timeout in milliseconds | `1000` | 1.0.0 | +| logging.level | SPLITD_LOG_LEVEL | Log level [`error`/`warning`/`info`/`debug`/`verbose`] | `error` | 1.0.0 | +| logging.output | SPLITD_LOG_OUTPUT | Log output | `/dev/stdout` | 1.0.1 | + + +### A word on IPC socket types + +`splitd` currently supports listenting for connection on two types of unix sockets: `STREAM` and `SEQPACKET`. + +Stream-based sockets operate in a similar fashion to TCP-based sockets, without message boundaries and with support for partial reads. Sequenced packet sockets, on the other hand, preserve message boundaries and require the reader to consume the whole package at once (with properly preallocated buffers on the reader side). Since sequenced packet sockets do not require framing/unframing and read with a single syscall, they tend to perform better than stream-based sockets, but they have limited support for large message sizes. With current splitd support for SDK `client.getTreatment()` and `client.getTreatments()` function calls, message size limits are not an issue, but once the manager functionality for querying Split feature flags or dynamic configs become available, it's possible to hit message size limits with the Split Thin SDK `client.getTreatmentsWithConfig()` or `manager.Splits()` function calls. + +For handling large payloads, the following options can be considered: +* Use `unix-stream` type sockets. (Note that configuration should be set on both splitd and the consumer application). +* Update the OS buffer sizes (`/proc/sys/net/core/wmem_max` and `/proc/sys/net/core/rmem_max`). An example can be seen [here](https://www.ibm.com/docs/de/smpi/10.2?topic=mpi-tuning-your-linux-system). + +## Using a network proxy + +If you need to use a network proxy, configure proxies by setting the environment variables **HTTP_PROXY** and **HTTPS_PROXY**. The internal HTTP client reads those variables and uses them to perform the server request. + +```bash title="Example: Environment variables" +$ export HTTP_PROXY="http://10.10.1.10:3128" +$ export HTTPS_PROXY="http://10.10.1.10:1080" +``` + +### Splitd service shutdown + +The splitd daemon handles `SIGTERM` gracefully, flushing all queued data to the backend before the application exits, and removing the socket file. Alternately a `SIGKILL` can be used to abort execution. diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-evaluator.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-evaluator.md new file mode 100644 index 00000000000..ce7a3b64b11 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-evaluator.md @@ -0,0 +1,749 @@ +--- +title: Split Evaluator +sidebar_label: Split Evaluator +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +Using Split involves using one of our SDKs. The Split team builds and maintains these SDKs for some of the most popular language libraries and the SDKs are available under open source licenses. For languages where there is no native SDK support, Split offers the [Split Evaluator](https://github.com/splitio/split-evaluator), a small service capable of evaluating some or all available features for a given customer via a REST endpoint. + +## Setup +The service is available via Docker or command line and its source code is available at [https://github.com/splitio/split-evaluator]( https://github.com/splitio/split-evaluator). + +### Docker (recommended) + * Pull the image: `docker pull splitsoftware/split-evaluator` + * Run as: +```bash +docker run \ + -e SPLIT_EVALUATOR_API_KEY={YOUR_SDK_KEY} \ + -p 7548:7548 splitsoftware/split-evaluator +``` + +:::note +See the configuration section for details on port usage, specifically the use of the `SPLIT_EVALUATOR_SERVER_PORT` environment variable. +::: + +:::info[Docker ports] +In the example above, the service can be reached via curl using port 7548. +::: + +### Command line +To install the service via command line: + +1. Clone the repository: `git clone https://github.com/splitio/split-evaluator` +2. Prepare the sources: `npm install` + +:::info[Previous Versions] +Split Evaluator `2.0.0` is a breaking change. This version adds supports for all the APIs that our current SDKs have but be aware of using some deprecated APIs if you continue to use an older version of this service. +::: + +```bash +SPLIT_EVALUATOR_API_KEY= \ +SPLIT_EVALUATOR_AUTH_TOKEN= \ +npm start +``` + +## Endpoints +The following section will describe the APIs that evaluator can manage. There are grouped in four different resources depending on what they want to achieve. +* **Client:** it corresponds to the APIs used to send impressions (get treatments) and track events. +* **Manager:** it corresponds to the APIs that will give you information of the available feature flags. +* **Admin:** it corresponds to the APIs that will give you information of the Split Evaluator itself. +* **api-docs:** it will contain the [Swagger](https://swagger.io/) specification of Split Evaluator. + +### api-docs + +#### Swagger +Split Evaluator uses Swagger to document all API endpoints available in this service. They are available by default in this url: [http://localhost:7548/api-docs](http://localhost:7548/api-docs). +If the port has been modified from the default(7548), make sure to set the environment variable SPLIT_EVALUATOR_SWAGGER_URL to match it. + +### Client APIs +Corresponds to the Client APIs that is generating impressions and tracking events. + * get-treatment + * get-treatments + * get-treatments-by-sets + * get-treatment-with-config + * get-treatments-with-config + * get-treatments-with-config-by-sets + * get-all-treatments + * get-all-treatments-with-config + * track + +#### /client/get-treatment +Evaluates a single feature flag for a single key. + +##### Query params + * **key:** The key used in the `getTreatment` call. + * **split-name:** The name of the feature flag you want to include in the `getTreatment` call. + * **bucketing-key:** (*Optional*) The bucketing key used in the `getTreatment` call. + * **attributes:** (*Optional*) A JSON string of the attributes to include in the `getTreatment` call of the SDK. + +Example: + +```bash +curl 'http://localhost:7548/client/get-treatment?key=my-customer-key&split-name=FEATURE_FLAG_NAME_1&attributes=\{"attribute1":"one","attribute2":2,"attribute3":true\}' -H 'Authorization: {SPLIT_EVALUATOR_AUTH_TOKEN}' +``` + +Response: + +```json + { + "splitName": "FEATURE_FLAG_NAME_1", + "treatment": "on" + } +``` + +#### /client/get-treatments +Provides a way of doing multiple evaluations at once. + +##### Query params + * **key:** The key used in the `getTreatments` call. + * **split-names:** The names of the feature flags you want to include in the `getTreatments` call separated by commas. + * **bucketing-key:** (*Optional*) The bucketing key used in the `getTreatments` call. + * **attributes:** (*Optional*) A JSON string of the attributes to include in the `getTreatments` call of the SDK. + +Example: + +```bash +curl 'http://localhost:7548/client/get-treatments?key=my-customer-key&split-names=FEATURE_FLAG_NAME_1,FEATURE_FLAG_NAME_2&attributes=\{"attribute1":"one","attribute2":2,"attribute3":true\}' -H 'Authorization: {SPLIT_EVALUATOR_AUTH_TOKEN}' +``` + +Response: + +```json + { + "FEATURE_FLAG_NAME_1": { + "treatment": "on" + }, + "FEATURE_FLAG_NAME_2": { + "treatment": "off" + } + } +``` + +#### /client/get-treatments-by-sets +Evaluates all flags that are part of the provided set names and are cached on the SDK instance. + +##### Query params + * **key:** The key used in the `getTreatmentsBySets` call. + * **flag-sets:** The names of the flag sets you want to include in the `getTreatmentsBySets` call separated by commas. + * **bucketing-key:** (*Optional*) The bucketing key used in the `getTreatmentsBySets` call. + * **attributes:** (*Optional*) A JSON string of the attributes to include in the `getTreatmentsBySets` call of the SDK. + +Example: + +```bash +curl 'http://localhost:7548/client/get-treatments-by-sets?key=my-customer-key&flag-sets=backend,server_side&attributes=\{"attribute1":"one","attribute2":2,"attribute3":true\}' -H 'Authorization: {SPLIT_EVALUATOR_AUTH_TOKEN}' +``` + +Response: + +```json + { + "FEATURE_FLAG_NAME_1": { + "treatment": "on" + }, + "FEATURE_FLAG_NAME_2": { + "treatment": "off" + } + } +``` + +#### /client/get-treatment-with-config +Evaluates a single feature flag for a single key and adds config in the result. + +##### Query params + * **key:** The key used in the `getTreatmentWithConfig` call. + * **split-name:** The name of the feature flag you want to include in the `getTreatmentWithConfig` call. + * **bucketing-key:** (*Optional*) The bucketing key used in the `getTreatmentWithConfig` call. + * **attributes:** (*Optional*) A JSON string of the attributes to include in the `getTreatmentWithConfig` call of the SDK. + +Example: + +```curl 'http://localhost:7548/client/get-treatment-with-config?key=my-customer-key&split-name=FEATURE_FLAG_NAME_1&attributes=\{"attribute1":"one","attribute2":2,"attribute3":true\}' -H 'Authorization: {SPLIT_EVALUATOR_AUTH_TOKEN}' +``` + +Response: +```json + { + "splitName": "FEATURE_FLAG_NAME_1", + "treatment": "on", + "config": "{/"color/": /"black/" }" + } +``` + +#### /client/get-treatments-with-config +Provides a way of doing multiple evaluations at once and attaches configs for each feature flag evaluated. + +##### Query params + * **key:** The key used in the `getTreatmentsWithConfig` call. + * **split-names:** The names of the feature flags you want to include in the `getTreatmentsWithConfig` call separated by commas. + * **bucketing-key:** (*Optional*) The bucketing key used in the `getTreatmentsWithConfig` call. + * **attributes:** (*Optional*) A JSON string of the attributes to include in the `getTreatmentsWithConfig` call of the SDK. + +Example: +```bash +curl 'http://localhost:7548/client/get-treatments-with-config?key=my-customer-key&split-names=FEATURE_FLAG_NAME_1,FEATURE_FLAG_NAME_2&attributes=\{"attribute1":"one","attribute2":2,"attribute3":true\}' -H 'Authorization: {SPLIT_EVALUATOR_AUTH_TOKEN}' +``` + +Response: + +```json + { + "FEATURE_FLAG_NAME_1": { + "treatment": "on", + "config": "{/"color/": /"black/" }" + }, + "FEATURE_FLAG_NAME_2": { + "treatment": "off", + "config": null + } + } +``` + +#### /client/get-treatments-with-config-by-sets +Provides a way of doing multiple evaluations at once and attaches configs for each feature flag that are part of the provided set names. + +##### Query params + * **key:** The key used in the `getTreatmentsWithConfigBySets` call. + * **flag-sets:** The names of the flag sets you want to include in the `getTreatmentsWithConfigBySets` call separated by commas. + * **bucketing-key:** (*Optional*) The bucketing key used in the `getTreatmentsWithConfigBySets` call. + * **attributes:** (*Optional*) A JSON string of the attributes to include in the `getTreatmentsWithConfigBySets` call of the SDK. + +Example: + +```bash +curl 'http://localhost:7548/client/get-treatments-with-config-by-sets?key=my-customer-key&flag-sets=backend,server_side&attributes=\{"attribute1":"one","attribute2":2,"attribute3":true\}' -H 'Authorization: {SPLIT_EVALUATOR_AUTH_TOKEN}' +``` + +Response: + +```json + { + "FEATURE_FLAG_NAME_1": { + "treatment": "on", + "config": "{/"color/": /"black/" }" + }, + "FEATURE_FLAG_NAME_2": { + "treatment": "off", + "config": null + } + } +``` + +#### /client/get-all-treatments +Performs multiple evaluations at once. In this case it will match all the feature flags for a given traffic-type and will perform a `getTreatments` call with the key provided. You can send more than one `{matchingKey,bucketingKey,trafficType}` object. + +##### Query params + * **keys:** The array of keys to be used in the `getTreatments` call. Each key should specify `matchingKey` and `trafficType`. You can also specify `bucketingKey`. + * **attributes:** (*optional*) A JSON string of the attributes to include in the `getTreatments` call of the SDK. + +Example: + +```bash +curl 'http://localhost:7548/client/get-all-treatments?keys=\[\{"matchingKey":"my-first-key","trafficType":"account"\},\{"matchingKey":"my-second-key","bucketingKey":"my-bucketing-key","trafficType":"user"\}\]&attributes=\{"attribute1":"one","attribute2":2,"attribute3":true\}' -H 'Authorization: {SPLIT_EVALUATOR_AUTH_TOKEN}' +``` + +Response: + +```json +{ + "account": { + "FEATURE_FLAG_NAME_1": { + "treatment": "on" + }, + "FEATURE_FLAG_NAME_2": { + "treatment": "off" + } + }, + "user": { + "FEATURE_FLAG_NAME_3": { + "treatment": "off" + } + } +} +``` + +#### /client/get-all-treatments-with-config +Performs multiple evaluations at once. In this case it will match all the feature flags for a given traffic-type and will perform a `getTreatmentsWithConfig` call with the key provided. You can send more than one `{matchingKey,bucketingKey,trafficType}` object. This endpoint will also adds the configurations for particular feature flag. + +##### Query params + * **keys:** The array of keys to be used in the `getTreatmentsWithConfig` call. Each key should specify `matchingKey` and `trafficType`. You can also specify `bucketingKey`. + * **attributes:** (*optional*) A JSON string of the attributes to include in the `getTreatmentsWithConfig` call of the SDK. + +Example: + +```bash +curl 'http://localhost:7548/client/get-all-treatments-with-config?keys=\[\{"matchingKey":"my-first-key","trafficType":"account"\},\{"matchingKey":"my-second-key","bucketingKey":"my-bucketing-key","trafficType":"user"\}\]&attributes=\{"attribute1":"one","attribute2":2,"attribute3":true\}' -H 'Authorization: {SPLIT_EVALUATOR_AUTH_TOKEN}' +``` + +Response: + +```json +{ + "account": { + "FEATURE_FLAG_NAME_1": { + "treatment": "on", + "config": "{/"color/": /"black/" }" + }, + "FEATURE_FLAG_NAME_2": { + "treatment": "off", + "config": null + } + }, + "user": { + "FEATURE_FLAG_NAME_3": { + "treatment": "off", + "config": "{/"copy/": /"better copy/" }" + } + } +} +``` + +#### /client/track +Records any actions your customers perform. Each action is known as an event and corresponds to an event type. Calling track allows you to measure the impact of your feature flags on your users' actions and metrics. + +##### Query params + * **key:** The key used in the `track` call. + * **traffic-type:** The traffic type of the key that you want to include in the `track` call. + * **event-type:** The event type that this event should correspond to the `track` call of the SDK. + * **value:** (*Optional*) value to be used in creating the metric. + * **properties:** (*Optional*) A JSON string of the properties to include in the `track` call of the SDK for filtering your metrics. + +Example: + +```bash +curl 'http://localhost:7548/client/track?key=my-customer-key&event-type=my-event&traffic-type=account&properties=\{"prop1":"one","prop2":2,"attribute3":true\}' -H 'Authorization: {SPLIT_EVALUATOR_AUTH_TOKEN}' +``` + +Response: +`Successfully queued event` + +### Manager APIs +Provides information of all the available feature flags. + * split + * splits + * names + +#### /manager/split +Provides information of one particular feature flag. + +##### Query params + * **split-name:** The name of the feature flag you want to have information. It uses `split` call of the SDK. + +Example: + +```bash +curl 'http://localhost:7548/manager/split?split-name=FEATURE_FLAG_NAME_1' -H 'Authorization: {SPLIT_EVALUATOR_AUTH_TOKEN}' +``` + +Response: + +```json + { + "name": "FEATURE_FLAG_NAME_1", + "trafficType": "user", + "killed": false, + "changeNumber": 1563394983932, + "treatments": ["on", "off"], + "configs": { + "on": "{/"color/": /"black/" }", + "off": "{/"color/": /"red/" }" + }, + "sets": [ + "backend", + "server_side" + ], + "defaultTreatment": "off" + } +``` + +#### /manager/splits +Provides information of all the available feature flags by calling `splits` method from the SDK. + +Example: + +```bash +curl 'http://localhost:7548/manager/splits' -H 'Authorization: {SPLIT_EVALUATOR_AUTH_TOKEN}' +``` + +Response: + +```json + { + "splits": [{ + "name": "FEATURE_FLAG_NAME_1", + "trafficType": "user", + "killed": false, + "changeNumber": 1563394983932, + "treatments": ["on", "off"], + "configs": { + "on": "{/"color/": /"black/" }", + "off": "{/"color/": /"red/" }" + }, + "sets": [ + "backend", + "server_side" + ], + "defaultTreatment": "off" + } + }, { + "name": "FEATURE_FLAG_NAME_2", + "trafficType": "user", + "killed": false, + "changeNumber": 1563394983933, + "treatments": ["on", "off"], + "configs": { + "on": "{/"color/": /"yellow/" }", + "off": "{/"color/": /"green/" }" + }, + "sets": [ + "backend" + ], + "defaultTreatment": "on" + } + }] + } +``` + +#### /manager/names +Provides the names of the available feature flags. It calls `names` of the SDK. + +Example: + +```bash +curl 'http://localhost:7548/manager/names' -H 'Authorization: {SPLIT_EVALUATOR_AUTH_TOKEN}' +``` + +Response: + +```json + { + "splits": ["FEATURE_FLAG_NAME_1", "FEATURE_FLAG_NAME_2"] + } +``` + +### Admin +Provides information about Split Evaluator itself. + * ping + * uptime + * healthcheck + * version + * machine + * stats + +#### /admin/ping +A ping endpoint to monitor the service status. If the service is running, it responds with `pong` and the HTTP status code `200`. + +Example: + +```bash +curl 'http://localhost:7548/admin/ping' +``` + +#### /admin/uptime +Returns the uptime of the service in a human-readable string. + +Example: + +```bash +curl 'http://localhost:7548/admin/uptime' +``` + +Response: + +```json +"5d 3h 36m 39s" +``` + +#### /admin/healthcheck +Checks that everything is working as expected before sending evaluations. It returns a status code of either `200` or `500`, depending on the result of the check, along with a message explaining the status. + +Example: + +```bash +curl 'http://localhost:7548/admin/healthcheck' +``` + +Response: + +```json +// For 200 status code +"Split Evaluator working as expected." +// For 500 status code +"Split evaluator engine not evaluating traffic properly." +``` + +#### /admin/version +Version information for the evaluator and the SDK within. + +Example: + +```bash +curl 'http://localhost:7548/admin/version' +``` + +Response: + +```json + { + "version": "1.0.2", + "sdk": "nodejs", + "sdkVersion": "9.3.4" + } +``` + +#### /admin/machine +Returns the data for the machine where this service is running. + +Example: + +```bash +curl 'http://localhost:7548/admin/machine' +``` + +Response: + +```json + { + "ip": "10.0.0.125", + "name": "machine_name" + } +``` + +#### /admin/stats +Returns information about evaluator like uptime and versions and the following stats from every environment: + * splitCount: Number of feature flags. + * segmentCount: Number of segments. + * lastSynchronization: + * splits: timestamp for last feature flags synchronization. + * segments: timestamp for last segments synchronization. + * impressions: timestamp for last impressions synchronization. + * impressionCount: timestamp for last impressionCount synchronization. + * events: timestamp for last events synchronization. + * telemetry: timestamp for last telemetry synchronization. + * token: timestamp for last token synchronization. + * timeUntilReady: time elapsed until environment reached ready state. + * httpErrors: information about http errors. + * ready: environment readiness status. + * impressionsMode: environment impressions mode. + +Example: + +```bash +curl 'http://localhost:7548/admin/stats' -H 'Authorization: {SPLIT_EVALUATOR_AUTH_TOKEN}' +``` + +Response: + +```json + { + "uptime": "10d 2h 1m 4s", + "healthcheck": { + "version": "2.3.0", + "sdk": "nodejs", + "sdkVersion": "10.22.0" + }, + "environments": { + "######tkn1": { + "splitCount": 20, + "segmentCount": 5, + "lastSynchronization": { + "splits": 1674855033186, + "segments": 1674855033286, + "mySegments": 1674855033386, + "impressions": 1674855033486, + "impressionCount": 1674855033586, + "events": 1674855033686, + "telemetry": 1674855033786, + "token": 1674855033886 + }, + "timeUntilReady": 833, + "httpErrors": {}, + "ready": true, + "impressionsMode": "OPTIMIZED" + }, + "######tkn2": { + "splitCount": 0, + "segmentCount": 0, + "lastSynchronization": {}, + "timeUntilReady": 0, + "httpErrors": { + "telemetry": { + "401": 1 + } + }, + "ready": false, + "impressionsMode": "OPTIMIZED" + } + } +} +``` + +## Impression listener + +The Split Evaluator provides an impression listener (`SPLIT_EVALUATOR_IMPRESSION_LISTENER_ENDPOINT`) that bulks post impressions to a user-defined HTTP endpoint. The endpoint should expect a POST request, containing a JSON body with the following format. + +If an impression listener is provided when the Split Evaluator is initialized, a task runs in background that posts impressions. There are two ways of posting impressions to the provided endpoint: + +* Every 30 seconds by the Evaluator. +* When the queue of impressions reached the max amount of impressions. + +For more information about how to configure the impression listener, refer to [Configuration](#configuration) section of this guide. + +```json + { + "impressions": [ + { + "testName": "my-experiment", + "keyImpressions": [ + { + "keyName": "key-experiment", + "treatment": "on", + "time": 987654321, + "changeNumber": 987654321, + "label": "label" + } + ] + }, + { + "testName": "my-experiment-2", + "keyImpressions": [ + { + "keyName": "key-experiment", + "treatment": "off", + "time": 123456789, + "changeNumber": 123456789, + "label": "default rule" + } + ] + } + ] + } +``` + +## Multiple environments support + +Split Evaluator allows you to set more than one environment. This means that it's possible to evaluate treatments for many Split SDK Keys. To use this feature, the evaluator requires that each Split SDK Key is paired with a custom authorization token (which can be any string) in the environment variable SPLIT_EVALUATOR_ENVIRONMENTS as is shown in the following example: + +```bash +SPLIT_EVALUATOR_ENVIRONMENTS='[{"API_KEY":"","AUTH_TOKEN":""},{"API_KEY":"","AUTH_TOKEN":""}]' npm start +``` + +The previous command line example initializes the Split Evaluator connected to two environments. To evaluate or retrieve flags on env1 using \{SDK_KEY_env1\}, the requests should be done with \{CUSTOM_AUTHENTICATION_1\} set as the Authorization header. + +Example: + +#### /manager/splits (For environment 1) + +This provides information of all the available feature flags by calling the `splits` method from the SDK initialized with \. + +Example: + +```bash +curl 'http://localhost:7548/manager/splits' -H 'Authorization: {YOUR_CUSTOM_AUTHENTICATION_1}' +``` + +#### /manager/splits (For environment 2) + +This provides information of all the available feature flags by calling the `splits` method from the SDK initialized with \. + +Example: + +```bash +curl 'http://localhost:7548/manager/splits' -H 'Authorization: {YOUR_CUSTOM_AUTHENTICATION_2}' +``` + +:::info +To configure flag sets when running multiple environments, flag set names must be defined in `FLAG_SET_FILTER` property of `SPLIT_EVALUATOR_ENVIRONMENTS` object + +Example: + +```bash +SPLIT_EVALUATOR_ENVIRONMENTS='[{"API_KEY":"","AUTH_TOKEN":"","FLAG_SET_FILTER":"backend"},{"API_KEY":"","AUTH_TOKEN":"","FLAG_SET_FILTER":"backend,server_side"}]' npm start +``` +::: + +## Global config + +The SDK exposes configuration parameters that you can use to optimize SDK performance. Each parameter is preset to a reasonable default. You can optionally override these default values when instantiating the SDK. + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| scheduler.featuresRefreshRate | The SDK polls Split servers for changes to feature rollout plans. This parameter controls this polling period in seconds. | 60 | +| scheduler.segmentsRefreshRate | The SDK polls Split servers for changes to segment definitions. This parameter controls this polling period in seconds. | 60 | +| scheduler.impressionsRefreshRate | The SDK sends information on who got what treatment at what time back to Split servers to power analytics. This parameter controls how often this data is sent to Split servers. The parameter is in seconds. | 300 | +| scheduler.impressionsQueueSize | The max amount of impressions we queue. If the queue is full, the SDK flushes the impressions and resets the timer. | 30000 | +| scheduler.eventsPushRate | The SDK sends tracked events to Split servers. This setting controls that flushing rate in seconds. | 60 | +| scheduler.eventsQueueSize | The max amount of events we queue. If the queue is full, the SDK flushes the events and resets the timer. | 500 | +| scheduler.telemetryRefreshRate | The SDK caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers in seconds. | 3600 seconds (1 hour) | +| startup.requestTimeoutBeforeReady | Time to wait for a request before the SDK is ready. If this time expires, Node.js SDK tries again `retriesOnFailureBeforeReady` times before notifying its failure to be `ready`. Zero means no timeout. | 15 | +| startup.retriesOnFailureBeforeReady | Number of quick retries we do while starting up the SDK. | 1 | +| startup.readyTimeout | Maximum amount of time in seconds to wait before notifying a timeout. Zero means no timeout, so no `SDK_READY_TIMED_OUT` event is fired. | 15 | +| sync.splitFilters | Filter specific feature flags to be synced and evaluated by the SDK. This is formed by a type string property and a list of string values for the given criteria. Using the types 'bySet' (recommended, flag sets are available in all tiers) or 'byName', pass an array of strings defining the query. If empty or unset, all feature flags are downloaded by the SDK. | [] | +| sync.impressionsMode | This configuration defines how impressions (decisioning events) are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Split. This is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, ALL impressions are queued and sent to Split; this is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. | `OPTIMIZED` | +| debug | Boolean flag or log level string ('ERROR', 'WARN', 'INFO', or 'DEBUG') for activating SDK logs. | false | +| streamingEnabled | Boolean flag to enable the streaming service as default synchronization mechanism. In the event of an issue with streaming, the SDK falls back to the polling mechanism. If false, the SDK polls for changes as usual without attempting to use streaming. | true | + +To set each of the parameters defined above, use the following syntax: + +Example: + +```bash +docker run \ + -e SPLIT_EVALUATOR_ENVIRONMENTS='[{"API_KEY":"","AUTH_TOKEN":"","FLAG_SET_FILTER":"backend,server_side"}]' \ + -e SPLIT_EVALUATOR_GLOBAL_CONFIG='{ + scheduler: { + featuresRefreshRate: 60, + segmentsRefreshRate: 60, + impressionsRefreshRate: 300, + impressionsQueueSize: 30000, + eventsPushRate: 60, + eventsQueueSize: 500, + telemetryRefreshRate: 3600 + }, + startup: { + readyTimeout: 5 + }, + sync: { + impressionsMode: 'OPTIMIZED' + }, + debug: false + }' + -p 7548:7548 splitsoftware/split-evaluator +``` + +## Configuration + +The available configuration variables are listed below. Always use `-e =` with Docker. + +:::note +For those configurations available on global config and environment variables (for example, refresh rates), the environment variable configuration takes precedence over global configuration.** +::: + +| **Variable** | **Description** | **Default** | +| --- | --- | --- | +| SPLIT_EVALUATOR_ENVIRONMENTS | String list of environments `"API_KEY":string, "AUTH_TOKEN":string}[]` | - | +| SPLIT_EVALUATOR_API_KEY | Split SDK key to authenticate against Split services. | - | +| SPLIT_EVALUATOR_AUTH_TOKEN | Authentication key used to authenticate every request via the Authorization header. This is **not** a Split SDK key but an arbitrary value defined by the user. | No authentication | +| SPLIT_EVALUATOR_GLOBAL_CONFIG | String SDK config for every environment. | - | +| SPLIT_EVALUATOR_LOG_LEVEL | Use for setting the log level for service (NONE|INFO|DEBUG|WARN|ERROR). | - | +| SPLIT_EVALUATOR_SERVER_PORT | TCP port of the server inside the container. When using in Docker, make sure to match the right side of `-p :` with the value of this variable. | 7548 | +| SPLIT_EVALUATOR_IMPRESSION_LISTENER_ENDPOINT | Use it for providing a webhook to send a bulk of Impressions | - | +| SPLIT_EVALUATOR_SPLITS_REFRESH_RATE | The SDK polls Split servers for changes to feature roll-out plans. This parameter controls this polling period in seconds. | 60 | +| SPLIT_EVALUATOR_SEGMENTS_REFRESH_RATE | The SDK polls Split servers for changes to segment definitions. This parameter controls this polling period in seconds. | 60 | +| SPLIT_EVALUATOR_METRICS_POST_RATE | The SDK sends diagnostic metrics to Split servers. This parameters controls this metric flush period in seconds. | 60 | +| SPLIT_EVALUATOR_IMPRESSIONS_POST_RATE | The SDK sends information on who got what treatment at what time back to Split servers to power analytics. This parameter controls how often this data is sent to Split servers. The parameter should be in seconds. | 60 | +| SPLIT_EVALUATOR_EVENTS_POST_RATE | The SDK sends tracked events to Split servers. This setting controls that flushing rate in seconds. | 60 | +| SPLIT_EVALUATOR_EVENTS_QUEUE_SIZE | The max amount of events we queue. If the queue is full, the SDK flushes the events and reset the timer. | 500 | +| SPLIT_EVALUATOR_SWAGGER_URL | The url used as base for any Swagger test curl commands. | http://localhost:7548 | +| SPLIT_EVALUATOR_IP_ADDRESSES_ENABLED | Flag to disable IP addresses and host name from being sent to the Split backend. | 'true' | + +## HTTPS/SSL + +This service does not currently support a secured connection. We recommend running this service in a redundant manner behind a load balancer such as AWS ELB or Nginx, with SSL termination. + +Contact [support@split.io](mailto:support@split.io) if you have questions. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-javascript-synchronizer-tools.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-javascript-synchronizer-tools.md new file mode 100644 index 00000000000..e91d1637fc8 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-javascript-synchronizer-tools.md @@ -0,0 +1,179 @@ +--- +title: Split JavaScript synchronizer tools +sidebar_label: Split JavaScript synchronizer tools +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + +This guide provides detailed information about our Split JavaScript Sync Tools library. All of our SDKs and libraries are open source. Refer to our [Split JavaScript Sync Tools GitHub repository](https://github.com/splitio/javascript-sync-tools) to see the source code. + +Split sync tools is an NPM package that includes the **JavaScript Synchronizer**. This synchronizer coordinates the sending and receiving of data to a remote datastore that all of your processes can share to pull data for the evaluation of treatments. Out of the box, the SDKs pluggable feature allows them to connect to a remote datastore, and so the JavaScript Synchronizer uses same datastore as the cache for your SDKs when it evaluates treatments. It also posts impression and event data and metrics generated by the SDKs back to Split servers, for exposure in the Split user interface or sending it to the data integration of your choice. + +## Language Support + +Split sync tools supports Node.js version 8 or later. To run the tools in other JavaScript environments, the target environment must support modern ES6 (ECMAScript 2015) syntax, and provide built-in support or a global polyfill for Promises and Web Fetch API. + +If you're looking for possible polyfill options, for Promise, refer to [es6-promise](https://github.com/stefanpenner/es6-promise) and for Fetch, refer to the lightweight [unfetch](https://unpkg.com/unfetch@latest/polyfill/index.js) or [whatwg-fetch](https://cdn.jsdelivr.net/npm/whatwg-fetch@latest/dist/fetch.umd.min.js). + +## Overall Architecture + +JavaScript Synchronizer executes as a single run script which performs the following actions: + +* **Fetch feature flags:** Retrieve your feature flag definitions from Split servers and write them into the storage. Keep in mind that you can use filters to granularly control which feature flags are synced into the storage. See the [Configuration](#configuration) section for more details. +* **Fetch segments:** Retrieve your set segments lists from Split servers and write them into the storage. +* **Post impressions:** Send to Split servers the stored impressions generated by the SDKs. +* **Post events:** Send to Split servers the stored events generated by the SDKs `.track` method. + +Unlike the [Split Go Synchronizer](https://help.split.io/hc/en-us/articles/360019686092-Split-Synchronizer-Proxy), the JavaScript Synchronizer executes as an script in a compatible JavaScript runtime environment like Node.js. + +## Initialization + +Set up the synchronizer in your code base with the following two steps: + +### 1. Install the library in your project + +The library is published using `npm`, so you can install it in your project with an NPM package manager. + + + +```bash +npm install --save @splitsoftware/splitio-sync-tools +``` + + +```bash +yarn add @splitsoftware/splitio-sync-tools +``` + + + +### 2. Import, instantiate, and execute the Synchronizer + + + +```javascript +const { Synchronizer } = require('@splitsoftware/splitio-sync-tools'); + +const synchronizer = new Synchronizer({ + core: { + authorizationKey: 'YOUR_SDK_KEY' + }, + storage: { + // Wrapper object must implement the interface used by the Synchronizer to read and write data into an storage + wrapper: MyStorageWrapper + } +}); + +synchronizer.execute().then(() => { + console.log('Single-run synchronization finished'); +}); +``` + + +```javascript +import { Synchronizer } from '@splitsoftware/splitio-sync-tools'; + +const synchronizer = new Synchronizer({ + core: { + authorizationKey: 'YOUR_SDK_KEY' + }, + storage: { + // Wrapper object must implement the interface used by the Synchronizer to read and write data into an storage + wrapper: MyStorageWrapper + } +}); + +await synchronizer.execute(); + +console.log('Single-run synchronization finished'); +``` + + + +## Configuration + +The JavaScript synchronizer has a number of knobs for configuring performance. Each knob is tuned to a reasonable default. However, you can override the values while instantiating the synchronizer. The parameters available for configuration are described below: + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| scheduler.impressionsPerPost | Maximum number of impressions to send per POST request. | 1000 | +| scheduler.eventsPerPost | Maximum number of events to send per POST request. | 1000 | +| scheduler.maxRetries | Maximum number of retry attempts for posting impressions and events. | 3 | +| sync.flagSpecVersion | The version of the feature flag definitions to be fetched and stored. | `1.1` | +| sync.splitFilters | List of Split filter objects to granularly control which feature flags are synced into the storage. This is formed by a type string property and a list of string values for the given criteria. Using the types 'bySet' (recommended, flag sets are available in all tiers) or 'byName', pass an array of strings defining the query. If empty or unset, all feature flags are synced. | [] | +| sync.impressionsMode | This configuration defines how impressions extracted from the storage are pre-processed before being sent to Split servers. Supported modes are OPTIMIZED and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Split. In DEBUG mode, ALL impressions are queued and sent to Split. Use DEBUG mode when you want every impression to be logged in the Split user interface when trying to debug your setup. | `OPTIMIZED` | +| sync.requestOptions.agent | A custom Node.js HTTP(S) Agent used to perform the requests to the Split servers. Go to the [Proxy](#proxy) section for details. | undefined | +| storage.prefix | An optional prefix for your data, to avoid collisions if using the same storage for multiple SDK keys. | `SPLITIO` | +| debug | Either a boolean flag or a log level string ('ERROR', 'WARN', 'INFO', or 'DEBUG') for activating Synchronizer logs. | false | + +To set each of the parameters defined above, use the following syntax: + + + + +```javascript +const synchronizer = new Synchronizer({ + core: { + authorizationKey: 'YOUR_SDK_KEY' + }, + scheduler: { + impressionsPerPost: 1000, + eventsPerPost: 1000, + maxRetries: 3 + }, + sync: { + splitFilters: [{ + type: 'bySet', + values: ['backend', 'server_side'] + }], + impressionsMode: 'DEBUG' + }, + storage: { + prefix: 'MYPREFIX', + wrapper: MyStorageWrapper + }, + debug: true +}); +``` + + + +## Proxy + +If you need to use a network proxy, you can provide a custom [Node.js HTTPS Agent](https://nodejs.org/api/https.html#class-httpsagent) by setting the `sync.requestOptions.agent` configuration variable. The Synchronizer will use this agent to perform requests to Split servers. + + + +```javascript +// Install with `npm install https-proxy-agent` +const { HttpsProxyAgent } = require('https-proxy-agent'); +const { Synchronizer } = require('@splitsoftware/splitio-sync-tools'); + +const proxyAgent = new HttpsProxyAgent(process.env.HTTPS_PROXY || 'http://10.10.1.10:1080'); + +const synchronizer = new Synchronizer({ + ... + sync: { + requestOptions: { + agent: proxyAgent + } + } +}); +``` + + + +## Examples + +This section contains a list of examples of wrapper implementations for different remote datastores that can be used with JavaScript Synchronizer. + +* [Cloudflare Workers template](https://github.com/splitio/cloudflare-workers-template) diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-proxy.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-proxy.md new file mode 100644 index 00000000000..cc0ec78f8d9 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-proxy.md @@ -0,0 +1,550 @@ +--- +title: Split Proxy +sidebar_label: Split Proxy +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +The Split Proxy enables you to deploy a service in your own infrastructure that behaves like Split's servers and is used by both server-side and client-side SDKs to synchronize the flags without connecting to Split's actual backend directly. + +This tool reduces connection latencies from the SDKs to the Split server to the SDKs transparently, and when a single connection is required from a private network to the outside for security reasons. + +### Architecture + +

+ Proxy architecture +

+ +## Setup + +### Docker (recommended) + +The service is available via Docker or command line, and its source code is available at the [split-synchronizer GitHub site](https://github.com/splitio/split-synchronizer). + + * Pull the image: `docker pull splitsoftware/split-proxy` + * Run as: + +```bash title="Standard execution" +docker run --rm --name split-proxy \ + -p 3000:3000 \ + -p 3010:3010 \ + -e SPLIT_PROXY_APIKEY="your-sdk-key" \ + -e SPLIT_PROXY_CLIENT_APIKEYS="123456,qwerty" \ + splitsoftware/split-proxy +``` + +:::info[API Keys] +The `SPLIT_PROXY_APIKEY` is the server-side SDK API Key that you can find or create in the Split UI in Admin settings. The Split Proxy uses the `SPLIT_PROXY_APIKEY` to connect to Split servers. + +The `SPLIT_PROXY_CLIENT_APIKEYS` is a list of strings that the Split Proxy will use to authenticate a client. (A Split Proxy client is a client/server-side Split SDK instance that connects to Split Proxy.) The Split Proxy will validate the client by comparing the key the client provides with the strings listed in `SPLIT_PROXY_CLIENT_APIKEYS`. These keys can be any string, generated via any method of your choice. For example, you can generate a GUID or use the string "hello" (e.g. for initial setup and testing the connection). As long as the Proxy client supplies a string that is in the `SPLIT_PROXY_CLIENT_APIKEYS` list, the Proxy will accept the client and forward the request to Split servers. The Split Proxy client (the client/server-side Split SDK instance) will supply a Split Proxy Client API Key in the usual place of the SDK Key. +::: + +### Command line + +To install and run the service from command line, depending of your platform, follow the steps below: + +#### Linux + +On Linux systems, invoke the Proxy service install script with the following: + +```bash title="Shell" +curl -L -o install_linux.bin 'https://downloads.split.io/proxy/install_split_proxy_linux.bin' && chmod 755 install_linux.bin && ./install_linux.bin +``` + +#### OSX + +On OSX systems, invoke the Proxy service install script with the following: + +```bash title="Shell" +curl -L -o install_osx.bin 'https://downloads.split.io/proxy/install_split_proxy_osx.bin' && chmod 755 install_osx.bin && ./install_osx.bin +``` + +#### Windows + +On Microsoft Windows systems, follow the steps below: + +1. Download the app from [https://downloads.split.io/proxy/split_proxy_windows.zip](https://downloads.split.io/proxy/split_proxy_windows.zip). + +2. Unzip the downloaded file. + +3. Run it. + +:::info[Download previous versions] +The links above point to the latest version. To download a previous version of split-sync, go to [https://downloads.split.io/proxy/downloads.proxy.html](https://downloads.split.io/proxy/downloads.proxy.html). +::: + +### Run the service + +To run the service, paste the following snippet into your command line terminal and add your SDK key. + +#### Linux/Mac + +```bash title="Proxy" +split-proxy -apikey "your_sdk_key" -client-apikeys "your_client_key_1,your_client_key_2" +``` + +#### Windows + +Open the cmd terminal or the PowerShell terminal, go to (cd) your unzipped Split Proxy folder, and type: + +```bash title="Proxy" +split-proxy -apikey "your_sdk_key" -client-apikeys "your_client_key_1,your_client_key_2" +``` + +#### Recommended configuration for production + +You can run the service with the simple steps above, but the system is more stable in your production environment when you run the job with a scheduling system. We recommend starting the Proxy using [supervisord](http://supervisord.org), a daemon that launches other processes and ensures they stay running. + +To use supervisord, make sure you install it on your machine. You can get help on the installation at the [official Supervisord documentation](http://supervisord.org/installing.html). + +After you install supervisord into your project, copy and paste the program below anywhere into the `supervisord.conf` file that should now be in your project. + +```ini title="supervisord configuration file (sample)" +[program:splitio_proxy] +command=/usr/local/bin/split-proxy -config /path/to/your/config.file.json +process_name = SplitIO +numprocs = 1 +autostart=true +autorestart=true +user = your_user +stderr_logfile=/var/log/splitio.err.log +stderr_logfile_maxbytes = 1MB +stdout_logfile=/var/log/splitio.out.log +stdout_logfile_maxbytes = 1MB +``` + +## Advanced configuration + +The Proxy service has several knobs for configuring performance. Each knob is tuned to a reasonable default. However, you can override the default values by changing a `splitio.config.json` file or by setting your customer values as parameters of `-config` in the command line option. In this section, we lay out all the different knobs you can configure for performance, persistent storage, and logging. + +The `splitio.config.json` file provided using the `-config` option lets you control how often the synchronizer fetches data from Split servers. You can create a sample JSON file automatically with default values by running the following command: + +```bash title="Shell" +./split-proxy -write-default-config "/home/someuser/splitio.config.json" +``` + +:::info[Configuration path file] +Save the JSON config file on your server in your desired folder. For example, on Linux systems, you can save it in the `etc` folder. Be sure to set the right path as the `-config` parameter. +::: + +```json title="splitio.config.json" +{ + "apikey": "YOUR_SDK_KEY", + "ipAddressEnabled": true, + "initialization": { + "timeoutMS": 10000, + "snapshot": "", + "forceFreshStartup": false + }, + "server": { + "apikeys": [ + "YOUR_SDK_KEY" + ], + "host": "0.0.0.0", + "port": 3000, + "httpCacheSize": 1000000 + }, + "admin": { + "host": "0.0.0.0", + "port": 3010, + "username": "", + "password": "", + "secureChecks": false + }, + "storage": { + "volatile": {}, + "persistent": { + "filename": "" + } + }, + "sync": { + "splitRefreshRateMs": 60000, + "segmentRefreshRateMs": 60000, + "advanced": { + "streamingEnabled": true, + "httpTimeoutMs": 30000, + "impressionsBufferSize": 500, + "eventsBufferSize": 500, + "telemetryBufferSize": 500, + "impressionsWorkers": 10, + "eventsWorkers": 10, + "telemetryWorkers": 10, + "internalTelemetryRateMs": 3600000 + } + }, + "integrations": { + "impressionListener": { + "endpoint": "", + "queueSize": 100 + }, + "slack": { + "webhook": "", + "channel": "" + } + }, + "logging": { + "level": "info", + "output": "stdout", + "rotationMaxFiles": 10, + "rotationMaxSizeKb": 0 + }, + "healthcheck": { + "dependencies": { + "dependenciesCheckRateMs": 3600000 + } + } +} +``` + +:::info[Command line parameters] +All available options in the JSON file are also included as command line options. Run the command followed by the `-help` option to see more details, or just keep reading this documentation page. +::: + +### Methods to Configure the Split Proxy + +You can configure the Split Proxy using the command line or by directly editing the above mentioned JSON configuration file. + +:::info[Config values priority] +All config values are set with a default value that you can see in the example JSON file above. You can overwrite the default value from the JSON config file, and you can overwrite the JSON config file from the command line. Refer to the sample below for how to do that using the command line. +::: + +```bash title="Shell" +split-proxy -config "/etc/splitio.config.json" -log-level=info -admin-username="admin" -admin-password="somePass" +``` + +### CLI Configuration options and its equivalents in JSON and Environment variables +The following table includes the available command line, JSON, and environment variable options and their descriptions. It specifies configuration options for the Split synchronizer. You can configure the synchronizer using command line arguments, environment variables when you run it as a docker container, and a JSON file when you run it locally. All of these configuration options can be used regardless of the configuration method. + +:::warning[Split Proxy v5.0 boolean options change] +With the Split synchronizer v5.0.0, the only accepted values for boolean flags are "true" and "false" in lowercase. Values such as "enabled", "on", "yes", or "True" result in an error when you start up. This applies to JSON, CLI arguments, and environment variables. +::: + +| **Command line option** | **JSON option** | **Environment variable** (container-only) | **Description** | +| --- | --- | --- | --- | +| log-level | level | SPLIT_PROXY_LOG_LEVEL | Log level (error|warning|info|debug|verbose). | +| log-output | output | SPLIT_PROXY_LOG_OUTPUT | Where to output logs (defaults to stdout). | +| log-rotation-max-files | rotationMaxFiles | SPLIT_PROXY_LOG_ROTATION_MAX_FILES | Max number of files to keep when rotating logs. | +| log-rotation-max-size-kb | rotationMaxSizeKb | SPLIT_PROXY_LOG_ROTATION_MAX_SIZE_KB | Max file size to keep before rotating log files. | +| admin-host | host | SPLIT_PROXY_ADMIN_HOST | Host where the admin server will listen. | +| admin-port | port | SPLIT_PROXY_ADMIN_PORT | Admin port where incoming connections will be accepted. | +| admin-username | username | SPLIT_PROXY_ADMIN_USERNAME | HTTP basic auth username for admin endpoints. | +| admin-password | password | SPLIT_PROXY_ADMIN_PASSWORD | HTTP basic auth password for admin endpoints. | +| admin-secure-hc | secureChecks | SPLIT_PROXY_ADMIN_SECURE_HC | Secure Healthcheck endpoints as well. | +| admin-tls-enabled | enabled | SPLIT_PROXY_ADMIN_TLS_ENABLED | Enable HTTPS on proxy endpoints. | +| admin-tls-client-validation | clientValidation | SPLIT_PROXY_ADMIN_TLS_CLIENT_VALIDATION | Enable client cert validation. | +| admin-tls-server-name | serverName | SPLIT_PROXY_ADMIN_TLS_SERVER_NAME | Server name as it appears in provided server-cert. | +| admin-tls-cert-chain-fn | certChainFn | SPLIT_PROXY_ADMIN_TLS_CERT_CHAIN_FN | X509 Server certificate chain. | +| admin-tls-private-key-fn | privateKeyFn | SPLIT_PROXY_ADMIN_TLS_PRIVATE_KEY_FN | PEM Private key file name. | +| admin-tls-client-validation-root-cert | clientValidationRootCertFn | SPLIT_PROXY_ADMIN_TLS_CLIENT_VALIDATION_ROOT_CERT. | X509 root cert for client validation | +| admin-tls-min-tls-version | minTlsVersion | SPLIT_PROXY_ADMIN_TLS_MIN_TLS_VERSION | Minimum TLS version to allow X.Y. | +| admin-tls-allowed-cipher-suites | allowedCipherSuites | SPLIT_PROXY_ADMIN_TLS_ALLOWED_CIPHER_SUITES | Comma-separated list of cipher suites to allow. | +| impression-listener-endpoint | endpoint | SPLIT_PROXY_IMPRESSION_LISTENER_ENDPOINT | HTTP endpoint to forward impressions to. | +| impression-listener-queue-size | queueSize | SPLIT_PROXY_IMPRESSION_LISTENER_QUEUE_SIZE | max number of impressions bulks to queue. | +| slack-webhook | webhook | SPLIT_PROXY_SLACK_WEBHOOK | slack webhook to post log messages. | +| slack-channel | channel | SPLIT_PROXY_SLACK_CHANNEL | slack channel to post log messages. | +| apikey | apikey | SPLIT_PROXY_APIKEY | Split Server-side SDK api-key. | +| ip-address-enabled | ipAddressEnabled | SPLIT_PROXY_IP_ADDRESS_ENABLED | Bundle host's ip address when sending data to Split. | +| timeout-ms | timeoutMS | SPLIT_PROXY_TIMEOUT_MS | How long to wait until the synchronizer is ready. | +| snapshot | snapshot | SPLIT_PROXY_SNAPSHOT | Snapshot file to use as a starting point. | +| force-fresh-startup | forceFreshStartup | SPLIT_PROXY_FORCE_FRESH_STARTUP | Wipe storage before starting the synchronizer | +| client-apikeys | apikeys | SPLIT_PROXY_CLIENT_APIKEYS | Apikeys that clients connecting to this Proxy will use. | +| server-host | host | SPLIT_PROXY_SERVER_HOST | Host/IP to start the proxy server on. | +| server-port | port | SPLIT_PROXY_SERVER_PORT | Port to listen for incoming requests from SDKs. | +| server-tls-enabled | enabled | SPLIT_PROXY_SERVER_TLS_ENABLED | Enable HTTPS on proxy endpoints. | +| server-tls-client-validation | clientValidation | SPLIT_PROXY_SERVER_TLS_CLIENT_VALIDATION | Enable client cert validation. | +| server-tls-server-name | serverName | SPLIT_PROXY_SERVER_TLS_SERVER_NAME | Server name as it appears in provided server-cert. | +| server-tls-cert-chain-fn | certChainFn | SPLIT_PROXY_SERVER_TLS_CERT_CHAIN_FN | X509 Server certificate chain. | +| server-tls-private-key-fn | privateKeyFn | SPLIT_PROXY_SERVER_TLS_PRIVATE_KEY_FN | PEM Private key file name. | +| server-tls-client-validation-root-cert | clientValidationRootCertFn | SPLIT_PROXY_SERVER_TLS_CLIENT_VALIDATION_ROOT_CERT | X509 root cert for client validation. | +| server-tls-min-tls-version | minTlsVersion | SPLIT_PROXY_SERVER_TLS_MIN_TLS_VERSION | Minimum TLS version to allow X.Y. | +| server-tls-allowed-cipher-suites | allowedCipherSuites | SPLIT_PROXY_SERVER_TLS_ALLOWED_CIPHER_SUITES | Comma-separated list of cipher suites to allow. | +| http-cache-size | httpCacheSize | SPLIT_PROXY_HTTP_CACHE_SIZE | How many responses to cache. | +| persistent-storage-fn | filename | SPLIT_PROXY_PERSISTENT_STORAGE_FN | Where to store flags and user-generated data. (Default: temporary file). | +| split-refresh-rate-ms | splitRefreshRateMs | SPLIT_PROXY_SPLIT_REFRESH_RATE_MS | How often to refresh feature flags. | +| segment-refresh-rate-ms | segmentRefreshRateMs | SPLIT_PROXY_SEGMENT_REFRESH_RATE_MS | How often to refresh segments. | +| streaming-enabled | streamingEnabled | SPLIT_PROXY_STREAMING_ENABLED | Enable/disable streaming functionality. | +| http-timeout-ms | httpTimeoutMs | SPLIT_PROXY_HTTP_TIMEOUT_MS | Total http request timeout. | +| impressions-workers | impressionsWorkers | SPLIT_PROXY_IMPRESSIONS_WORKERS | #workers to forward impressions to Split servers. | +| events-workers | eventsWorkers | SPLIT_PROXY_EVENTS_WORKERS | #workers to forward events to Split servers. | +| telemetry-workers | telemetryWorkers | SPLIT_PROXY_TELEMETRY_WORKERS | #workers to forward telemetry to Split servers. | +| internal-metrics-rate-ms | internalTelemetryRateMs | SPLIT_PROXY_INTERNAL_METRICS_RATE_MS | How often to send internal metrics. | +| dependencies-check-rate-ms | dependenciesCheckRateMs | SPLIT_PROXY_DEPENDENCIES_CHECK_RATE_MS | How often to check dependecies health. | + +## Listener + +The Split Proxy provides an impression listener that bulks post impressions to a user-defined HTTP endpoint. + +The endpoint should expect a POST request that contains a JSON body using the following format: + +```json title="JSON Impression" +{ + "impressions": [ + { + "testName": "feature1", + "keyImpressions": [ + { + "keyName": "user1", + "treatment": "on", + "time": 1502754901182, + "changeNumber": -1, + "label": "" + }, + { + "keyName": "user2", + "treatment": "off", + "time": 1502754876144, + "changeNumber": -1, + "label": "" + } + ] + } + ], + "sdkVersion": "php-5.2.2", + "machineIP": "208.63.222.7", + "MachineName": "" +} +``` + +The configuration options are available in the `integrations.impressionListener` section of the JSON configuration file detailed in [Advanced configuration](#advanced-configuration) section. + +## Using a network proxy + +If you need to use a network proxy, configure the proxies by setting the environment variables as HTTP_PROXY and HTTPS_PROXY. The internal HTTP client reads those variables and uses them to perform a server request. + +```bash title="Example: Environment variables" +$ export HTTP_PROXY="http://10.10.1.10:3128" +$ export HTTPS_PROXY="http://10.10.1.10:1080" +``` + +## Admin tools + +### Endpoints + +The `split-proxy` service has a set of endpoints and a dashboard that lets the DevOps and infra team monitor its status and cached data in real-time. By default, the port is `3010` and for security reason, it supports HTTP Basic Authentication configured by the user. + +**/info/ping** + +A ping endpoint to monitor the service status. If the service is running, it sends the text response `pong` and the HTTP status code `200`. + +**/info/version** + +Returns the `split-proxy` version in JSON format. + +```json +{ + "version" : "1.1.0" +} +``` + +**/info/uptime** + +Returns the uptime string representation in JSON format. + +```json +{ + "uptime" : "5d 3h 36m 39s" +} +``` + +**/info/config** + +Returns a JSON object describing the current configuration of the proxy. + +```json +{ + "config": { + "apikey": "*", + "ipAddressEnabled": true, + "initialization": { + "timeoutMS": 10000, + "snapshot": "", + "forceFreshStartup": false + }, + "server": { + "apikeys": [ + "YOUR_SDK_KEY" + ], + "host": "0.0.0.0", + "port": 3000, + "httpCacheSize": 1000000 + }, + "admin": { + "host": "0.0.0.0", + "port": 3010, + "username": "", + "password": "", + "secureChecks": false + }, + "storage": { + "volatile": {}, + "persistent": { + "filename": "" + } + }, + "sync": { + "splitRefreshRateMs": 60000, + "segmentRefreshRateMs": 60000, + "advanced": { + "streamingEnabled": true, + "httpTimeoutMs": 30000, + "impressionsBufferSize": 500, + "eventsBufferSize": 500, + "telemetryBufferSize": 500, + "impressionsWorkers": 10, + "eventsWorkers": 10, + "telemetryWorkers": 10, + "internalTelemetryRateMs": 3600000 + } + }, + "integrations": { + "impressionListener": { + "endpoint": "", + "queueSize": 100 + }, + "slack": { + "webhook": "", + "channel": "" + } + }, + "logging": { + "level": "info", + "output": "stdout", + "rotationMaxFiles": 10, + "rotationMaxSizeKb": 0 + }, + "healthcheck": { + "dependencies": { + "dependenciesCheckRateMs": 3600000 + } + } + } +} +``` + +**/health/application** + +Returns a JSON object describing whether the proxy is healthy or not. + +```json +{ + "healthy": true, + "healthySince": "2021-11-20T19:17:23.372708-03:00", + "items": [ + { + "name": "Feature flags", + "healthy": true, + "lastHit": "2021-11-20T19:17:36.147349-03:00" + }, + { + "name": "Segments", + "healthy": true, + "lastHit": "2021-11-20T19:17:36.324172-03:00" + } + ] +} + +``` + +**/health/dependencies** + +Returns a JSON object describing whether the servers the proxy depends on are healthy or not. + +```json +{ + "serviceStatus": "healthy", + "dependencies": [ + { + "service": "https://telemetry.split.io/health", + "healthy": true, + "healthySince": "2021-11-20T19:17:23.372741-03:00" + }, + { + "service": "https://auth.split.io/health", + "healthy": true, + "healthySince": "2021-11-20T19:17:23.372752-03:00" + }, + { + "service": "https://sdk.split.io/api/version", + "healthy": true, + "healthySince": "2021-11-20T19:17:23.372753-03:00" + }, + { + "service": "https://events.split.io/api/version", + "healthy": true, + "healthySince": "2021-11-20T19:17:23.372755-03:00" + }, + { + "service": "https://streaming.split.io/health", + "healthy": true, + "healthySince": "2021-11-20T19:17:23.372755-03:00" + } + ] +} + +``` + +**/admin/snapshot** + +Returns a binary snapshot file that can be used with the snapshot environment variable or command line argument when starting up the Split Proxy. + + +### Admin Dashboard + +Split-proxy has a web admin user interface out of the box that exposes all available endpoints. Browse to `/admin/dashboard` to see it. + +

+ proxy_dashboard_main.png +

+ +

+ proxy_dashboard_stats.png +

+ +The dashboard is organized into four sections for easy visualization: + +* **Dashboard:** Tile-sorted summary information, including these metrics: + - **Uptime**: Uptime metric + - **Healthy Since**: Time passed without errors + - **Logged Errors**: Total count of error messages + - **SDKs Total Hits**: Total SDKs requests + - **Backend Total Hits**: Total backend requests between split-proxy and Split servers + - **Cached Feature flags**: Number of feature flags cached in memory + - **Cached Segments**: Number of segments cached in memory + - **SDK Server**: displays the status of Split server for SDK + - **Events Server**: displays the status of Split server for Events + - **Streaming Server**: displays the status of Split streaming service + - **Auth Server**: displays the status of Split server for initial streaming authentication + - **Telemetry Server**: displays the status of Split server for telemetry capturing + - **Storage**: (only Sync mode) displays the status of the storage + - **Sync**: displays the status of the Proxy + - **Last Errors Log**: List of the last 10 error messages +* **SDK stats**: Metrics numbers and a latency graph, measured between SDKs requests integration and proxy +* **Split stats**: Metrics numbers and a latency graph, measured between proxy requests integration with Split servers +* **Data inspector**: Cached data showing feature flags and segments; filters to find keys and feature flag definitions +

+ proxy_dashboard_data.png +

+ +:::warning[Dashboard refresh rate] +The dashboard numbers are committed every 60 seconds. The `Logged Errors`, `Last Errors Log` tiles, and `Data inspector` section are populated each time the dashboard is refreshed. + +For Impressions and Events Queue size, the numbers are refreshed every 10 seconds. +::: + +### Service shutdown + +The `split-proxy` service can catch a `kill sig` command and start a graceful shutdown, flushing all cached data progressively. Additionally, you can perform `graceful stop` and `force stop (kill -9)` with one click from the admin dashboard. + +

+ sync5.png +

+ +If you configure a Slack channel and a Slack Webhook URL, an alert is sent to the channel when an initialization or shutdown is performed. + +

+ sync6.png +

diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-synchronizer.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-synchronizer.md new file mode 100644 index 00000000000..b57bc849805 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-synchronizer.md @@ -0,0 +1,921 @@ +--- +title: Split Synchronizer +sidebar_label: Split Synchronizer +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +By default, Split’s SDKs keep segment and feature flag data synchronized as users navigate across disparate systems, treatments, and conditions. However, some languages, do not have a native capability to keep a shared local cache of this data to properly serve treatments. For these cases, we built the Split Synchronizer service. + +This tool coordinates the sending and receiving of data to a remote datastore that all of your processes can share to pull data for the evaluation of treatments. Out of the box, Split supports Redis as a remote datastore, and so the Split Synchronizer uses Redis as the cache for your SDKs when evaluating treatments. It also posts impression and event data and metrics generated by the SDKs back to Split’s servers, for exposure in the user interface or sending to the data integration of your choice. The Synchronizer service runs as a standalone process in dedicated or shared servers and it does not affect the performance of your code, or Split’s SDKs. + +:::info[Split Synchronizer version 5.0 available!] +Since version 5.0.0 of the split-synchronizer, there's only one operation mode. What was once `proxy mode` is now a separate tool called [**Split proxy**](https://help.split.io/hc/en-us/articles/4415960499213-Split-Proxy). This version includes a more performant way to evict impressions & events from redis that allows customers to handle much greater volumes of data, while protecting your flags from being evicted if the redis instance runs low on memory. + +Currently, the SDKs supported by this versions are: +Ruby 6.0.0 +PHP 6.0.0 +Node.js 10.6.0 +Go 3.0.0 +Python 7.0.0 +.NET 4.0.0 +Java 4.4.0 +::: + +:::warning +If you are upgrading from Split synchronizer version 4.x or below to 5.x, some of the configuration and environment parameter names have been changed. Refer to the Configuration section and modify the parameters names accordingly. +::: + +## Supported SDKs + +The Split Synchronizer works with most of the languages that Split supports. + +* [PHP SDK](https://help.split.io/hc/en-us/articles/360020350372) +* [Python SDK](https://help.split.io/hc/en-us/articles/360020359652) +* [.NET SDK](https://help.split.io/hc/en-us/articles/360020240172) +* [Node.js SDK](https://help.split.io/hc/en-us/articles/360020564931) +* [Ruby SDK](https://help.split.io/hc/en-us/articles/360020673251) +* [Go SDK](https://help.split.io/hc/en-us/articles/360020093652) +* [Java SDK](https://help.split.io/hc/en-us/articles/360020405151) + +If you are looking for a language that is not listed here, contact the support team at [support@split.io](mailto:support@split.io) to discuss your options. + +## Overall Architecture + +**The service performs five actions:** + +* **Fetch feature flags:** Retrieve the feature flag definitions. +* **Fetch segments:** Retrieve your set segments lists. +* **Post impressions:** Send to Split servers the generated impressions by the SDK. +* **Post telemetry:** Send to Split servers different metrics of the SDK, such as latencies. +* **Post events:** Send to Split servers the generated events by the SDK `.track` method. + +:::info[Split-Sync v5.0.0 pipelined data eviction] +Starting with split-sync v5.0.0, we've introduced a new approach to impressions and events eviction. This replaces the previous approach of periodically fetching & posting impressions and events. Our new approch feature flags this task in 3 parts, a thread dedicated to fetching data from redis and placing it in a buffer, N threads (where N is derived from the number of available CPU cores) dedicated to parsing, formatting the data and placing it in a second buffer, and N (configurable) threads that pick the data and post it to Split servers. The result is a significant increase in throughput, that will better suit customers which operate on big volumes of data. +::: + +### Architecture + +

+ Split synchronizer architecture diagram +

+ +## Setup + +### Docker (recommended) + +The service is available via Docker or command line and its source code is available at https://github.com/splitio/split-synchronizer. + + * Pull the image: `docker pull splitsoftware/split-synchronizer` + * Run as: + + + + +```bash +docker run --rm --name split-synchronizer \ + -p 3010:3010 \ + -e SPLIT_SYNC_APIKEY="your-sdk-key" \ + -e SPLIT_SYNC_REDIS_HOST= \ + -e SPLIT_SYNC_REDIS_PORT= \ + splitsoftware/split-synchronizer +``` + + + + +```bash +docker run --rm --name split-synchronizer \ + -p 3010:3010 \ + -e SPLIT_SYNC_APIKEY="your-sdk-key" \ + -e SPLIT_SYNC_REDIS_SENTINEL_REPLICATION="true" \ + -e SPLIT_SYNC_REDIS_SENTINEL_MASTER="MASTER_SERVICE_NAME" \ + -e SPLIT_SYNC_REDIS_SENTINEL_ADDRESSES="SENTINEL_HOST_1:SENTINEL_PORT_1,SENTINEL_HOST_2:SENTINEL_PORT_2" \ + splitsoftware/split-synchronizer +``` + + + + +```bash +docker run --rm --name split-synchronizer \ + -p 3010:3010 \ + -e SPLIT_SYNC_APIKEY="your-sdk-key" \ + -e SPLIT_SYNC_REDIS_CLUSTER_MODE="true" \ + -e SPLIT_SYNC_REDIS_CLUSTER_NODES="CLUSTER_NODE_1:CLUSTER_PORT_1,CLUSTER_NODE_2:CLUSTER_PORT_2,CLUSTER_NODE_3:CLUSTER_PORT_3" \ + splitsoftware/split-synchronizer +``` + + + + +```bash +docker run --rm --name split-synchronizer \ + -p 3010:3010 \ + -e SPLIT_SYNC_APIKEY="your-sdk-key" \ + -e SPLIT_SYNC_REDIS_CLUSTER_MODE="true" \ + -e SPLIT_SYNC_REDIS_CLUSTER_NODES="Cluster Entry Host" \ + splitsoftware/split-synchronizer +``` + + + + +:::info[Synchronizer mode with local redis instance] +Sometimes, when building POCs or testing the synchronizer locally, you might want to launch our docker container image, pointing to a local redis server (or another container with redis, whose port has been mapped to a local one). In such case, you should consider adding the option `--network="host"` (appending it to the command shown above) when launching the synchronizer. This will allow you to use `-e SPLIT_SYNC_REDIS_HOST="localhost"`, with the split-synchronizer container properly reaching your local redis server. +::: + +:::info[Docker configuration] +The [Advanced configuration section](#advanced-configuration) includes additional Docker information in the column **Docker environment variable**. +::: + +### Command line + +To install and run the service from command line, follow the steps below depending of your platform. + +#### Linux + +On Linux systems, the Synchronizer service install script is invoked with this. + +```bash +curl -L -o install_linux.bin 'https://downloads.split.io/synchronizer/install_split_sync_linux.bin' && chmod 755 install_linux.bin && ./install_linux.bin +``` + +#### OSX + +On OSX systems, the Synchronizer service install script is invoked with this. + +```bash +curl -L -o install_osx.bin 'https://downloads.split.io/synchronizer/install_split_sync_osx.bin' && chmod 755 install_osx.bin && ./install_osx.bin +``` + +#### Windows + +On Microsoft Windows systems, follow these steps: + +1. Download the app from [https://downloads.split.io/synchronizer/split_sync_windows.zip](https://downloads.split.io/synchronizer/split_sync_windows.zip). + +2. Unzip the downloaded file. + +3. Run it! + +:::info[Download previous versions] +The links above point to the latest version. To download a previous version of split-sync, go to [https://downloads.split.io/synchronizer/downloads.sync.html](https://downloads.split.io/synchronizer/downloads.sync.html). +::: + +### Run the service + +To run the service, paste the snippet below into your command line terminal and add in your **SDK-Key**. + +#### Linux/Mac + +```bash +split-sync -apikey "your_sdk_key" +``` + +#### Windows + +Open the cmd terminal or the PowerShell terminal, go to (cd) unzipped Split Synchronizer folder, and type: + +```bash +split-sync.exe -apikey "your_sdk_key" +``` + +:::warning[Redis instance] +On the samples above, Redis is running as a local service with **default host: localhost** and **default port: 6379**. For further information, see [Advanced configuration](#advanced-configuration). +::: + +:::warning[Redis database] +To maximize performance and isolation, we recommend connecting to a Redis database dedicated to the Split Synchronizer. For further information, see [Advanced configuration](#advanced-configuration). +::: + +:::info[Redis Sentinel support] +Split Synchronizer also supports Redis Sentinel (v2) replication. For further information about Redis Sentinel, refer to the [Sentinel Documentation](https://redis.io/topics/sentinel). +::: + +:::info[Redis Cluster support] +Split Synchronizer supports Redis Cluster with Redis^3.0.0. For further information about Redis Cluster, refer to the [Cluster Documentation](https://redis.io/topics/cluster-spec). +::: + +#### Recommended configuration for production + +You can run the service with the simple steps above, but the system is more stable in your production environment when you run the job with a scheduling system. We recommend starting the synchronizer via [supervisord](http://supervisord.org), a daemon that launches other processes and ensures they stay running. + +To use supervisord, make sure that it is installed on your machine. You can get help on the installation at the [official Supervisord documentation](http://supervisord.org/installing.html). + +When supervisord is installed into your project, copy and paste the program below anywhere into the `supervisord.conf` file that should now be in your project. + +```ini title="supervisord configuration file (sample) +[program:splitio_sync] +command=/usr/local/bin/split-sync -config /path/to/your/config.file.json +process_name = SplitIO +numprocs = 1 +autostart=true +autorestart=true +user = your_user +stderr_logfile=/var/log/splitio.err.log +stderr_logfile_maxbytes = 1MB +stdout_logfile=/var/log/splitio.out.log +stdout_logfile_maxbytes = 1MB +``` + +## Advanced configuration + +The Synchronizer service has a number of knobs for configuring performance. Each knob is tuned to a reasonable default, however, you can override the default values by changing a `splitio.config.json` file or by setting your customer values as parameters of `-config` in the command line option. In this section, we lay out all the different knobs you can configure for performance, Redis, and logging. + +The `splitio.config.json` file provided via the `-config` option lets you control how often the synchronizer fetches data from Split servers. You can create a sample JSON file automatically with default values by running this command. + +```bash +./split-sync -write-default-config "/home/someuser/splitio.config.json" +``` + +:::info[Configuration path file] +Save the JSON config file on your server in your desired folder. For instance, on Linux systems, it could be saved in the `etc` folder. +Remember to set the right path as the `-config` parameter. +::: + + + + +```json +{ + "apikey": "YOUR_SDK_KEY", + "ipAddressEnabled": true, + "initialization": { + "timeoutMS": 10000, + "forceFreshStartup": false + }, + "storage": { + "type": "redis", + "redis": { + "host": "localhost", + "port": 6379, + "db": 0, + "username": "", + "password": "", + "prefix": "", + "network": "tcp", + "maxRetries": 0, + "dialTimeout": 5, + "readTimeout": 10, + "writeTimeout": 5, + "poolSize": 10, + "sentinelReplication": false, + "sentinelAddresses": "", + "sentinelMaster": "", + "clusterMode": false, + "clusterNodes": "", + "keyHashTag": "", + "enableTLS": false, + "tlsServerName": "", + "caCertificates": null, + "tlsSkipNameValidation": false, + "tlsClientCertificate": "", + "tlsClientKey": "" + } + }, + "sync": { + "splitRefreshRateMs": 60000, + "segmentRefreshRateMs": 60000, + "impressionsMode": "optimized", + "advanced": { + "streamingEnabled": true, + "httpTimeoutMs": 30000, + "internalTelemetryRateMs": 3600000, + "telemetryPushRateMs": 60000, + "impressionsFetchSize": 0, + "impressionsProcessConcurrency": 0, + "impressionsProcessBatchSize": 0, + "impressionsPostConcurrency": 0, + "impressionsPostSize": 0, + "impressionsAccumWaitMs": 0, + "eventsFetchSize": 0, + "eventsProcessConcurrency": 0, + "eventsProcessBatchSize": 0, + "eventsPostConcurrency": 0, + "eventsPostSize": 0, + "eventsAccumWaitMs": 0 + } + }, + "admin": { + "host": "0.0.0.0", + "port": 3010, + "username": "", + "password": "", + "secureChecks": false + }, + "integrations": { + "impressionListener": { + "endpoint": "", + "queueSize": 100 + }, + "slack": { + "webhook": "", + "channel": "" + } + }, + "logging": { + "level": "info", + "output": "stdout", + "rotationMaxFiles": 10, + "rotationMaxSizeKb": 0 + }, + "healthcheck": { + "app": { + "storageCheckRateMs": 3600000 + } + } +} +``` + + + + +```json +{ + "apikey": "YOUR_SDK_KEY", + "ipAddressEnabled": true, + "initialization": { + "timeoutMS": 10000, + "forceFreshStartup": false + }, + "storage": { + "type": "redis", + "redis": { + "host": "localhost", + "port": 6379, + "db": 0, + "username": "", + "password": "", + "prefix": "", + "network": "tcp", + "maxRetries": 0, + "dialTimeout": 5, + "readTimeout": 10, + "writeTimeout": 5, + "poolSize": 10, + "sentinelReplication": true, + "sentinelAddresses": "SENTINEL_HOST_1:SENTINEL_PORT_1, SENTINEL_HOST_2:SENTINEL_PORT_2,SENTINEL_HOST_3:SENTINEL_PORT_3", + "sentinelMaster": "MASTER_SERVICE_NAME", + "clusterMode": false, + "clusterNodes": "", + "keyHashTag": "", + "enableTLS": false, + "tlsServerName": "", + "caCertificates": null, + "tlsSkipNameValidation": false, + "tlsClientCertificate": "", + "tlsClientKey": "" + } + }, + "sync": { + "splitRefreshRateMs": 60000, + "segmentRefreshRateMs": 60000, + "impressionsMode": "optimized", + "advanced": { + "streamingEnabled": true, + "httpTimeoutMs": 30000, + "internalTelemetryRateMs": 3600000, + "telemetryPushRateMs": 60000, + "impressionsFetchSize": 0, + "impressionsProcessConcurrency": 0, + "impressionsProcessBatchSize": 0, + "impressionsPostConcurrency": 0, + "impressionsPostSize": 0, + "impressionsAccumWaitMs": 0, + "eventsFetchSize": 0, + "eventsProcessConcurrency": 0, + "eventsProcessBatchSize": 0, + "eventsPostConcurrency": 0, + "eventsPostSize": 0, + "eventsAccumWaitMs": 0 + } + }, + "admin": { + "host": "0.0.0.0", + "port": 3010, + "username": "", + "password": "", + "secureChecks": false + }, + "integrations": { + "impressionListener": { + "endpoint": "", + "queueSize": 100 + }, + "slack": { + "webhook": "", + "channel": "" + } + }, + "logging": { + "level": "info", + "output": "stdout", + "rotationMaxFiles": 10, + "rotationMaxSizeKb": 0 + }, + "healthcheck": { + "app": { + "storageCheckRateMs": 3600000 + } + } +} +``` + + + + +```json +{ + "apikey": "", + "ipAddressEnabled": true, + "initialization": { + "timeoutMS": 10000, + "forceFreshStartup": false + }, + "storage": { + "type": "redis", + "redis": { + "host": "localhost", + "port": 6379, + "db": 0, + "username": "", + "password": "", + "prefix": "", + "network": "tcp", + "maxRetries": 0, + "dialTimeout": 5, + "readTimeout": 10, + "writeTimeout": 5, + "poolSize": 10, + "sentinelReplication": false, + "sentinelAddresses": "", + "sentinelMaster": "", + "clusterMode": true, + "clusterNodes": "CLUSTER_NODE_1:CLUSTER_PORT_1, CLUSTER_NODE_2:CLUSTER_PORT_2,CLUSTER_NODE_3:CLUSTER_PORT_3", + "keyHashTag": "", + "enableTLS": false, + "tlsServerName": "", + "caCertificates": null, + "tlsSkipNameValidation": false, + "tlsClientCertificate": "", + "tlsClientKey": "" + } + }, + "sync": { + "splitRefreshRateMs": 60000, + "segmentRefreshRateMs": 60000, + "impressionsMode": "optimized", + "advanced": { + "streamingEnabled": true, + "httpTimeoutMs": 30000, + "internalTelemetryRateMs": 3600000, + "telemetryPushRateMs": 60000, + "impressionsFetchSize": 0, + "impressionsProcessConcurrency": 0, + "impressionsProcessBatchSize": 0, + "impressionsPostConcurrency": 0, + "impressionsPostSize": 0, + "impressionsAccumWaitMs": 0, + "eventsFetchSize": 0, + "eventsProcessConcurrency": 0, + "eventsProcessBatchSize": 0, + "eventsPostConcurrency": 0, + "eventsPostSize": 0, + "eventsAccumWaitMs": 0 + } + }, + "admin": { + "host": "0.0.0.0", + "port": 3010, + "username": "", + "password": "", + "secureChecks": false + }, + "integrations": { + "impressionListener": { + "endpoint": "", + "queueSize": 100 + }, + "slack": { + "webhook": "", + "channel": "" + } + }, + "logging": { + "level": "info", + "output": "stdout", + "rotationMaxFiles": 10, + "rotationMaxSizeKb": 0 + }, + "healthcheck": { + "app": { + "storageCheckRateMs": 3600000 + } + } +} +``` + + + + + +:::info[Command line parameters] +All the options available in the JSON file are also included as command line options. Run the command followed by the `-help` option to see more details, or just keep reading this documentation page. +::: + +### Methods to configure the Split synchronizer + +You can configure the Split synchronizer service using the command line or by directly editing the above mentioned **JSON** configuration file. + +:::info[Config values priority] +All config values are set with a default value that you can see in the example **JSON** file above. You can overwrite the default value from the JSON config file, and you can overwrite the JSON config file from the command line. See a sample below for how to do that via command line. +::: + +```bash +split-sync -config "/etc/splitio.config.json" -log-level "debug" -redis-pass "somePass" +``` + +### CLI Configuration options and its equivalents in JSON & Environment variables + +:::warning[Split Synchronizer version 5.0 boolean options change] +In order to reduce the issues because of typos and confusion due to "multiple words & case meaning the same", since version 5.0.0 of the split-synchronizer, the only accepted values for boolean flags are "true" & "false" in lowercase. Things like "enabled", "on", "yes", "tRue" will result in an error at startup. This applies to JSON, CLI arguments & environment variables. +::: + + +| **Command line option** | **JSON option** | **Environment variable (container-only)** | **Description** | +| --- | --- | --- | --- | +| log-level | level | SPLIT_SYNC_LOG_LEVEL | Log level (error|warning|info|debug|verbose) | +| log-output | output | SPLIT_SYNC_LOG_OUTPUT | Where to output logs (defaults to stdout) | +| log-rotation-max-files | rotationMaxFiles | SPLIT_SYNC_LOG_ROTATION_MAX_FILES | Max number of files to keep when rotating logs | +| log-rotation-max-size-kb | rotationMaxSizeKb | SPLIT_SYNC_LOG_ROTATION_MAX_SIZE_KB | Max file size before rotating log files. | +| admin-host | host | SPLIT_SYNC_ADMIN_HOST | Host where the admin server will listen | +| admin-port | port | SPLIT_SYNC_ADMIN_PORT | Admin port where incoming connections will be accepted | +| admin-username | username | SPLIT_SYNC_ADMIN_USERNAME | HTTP basic auth username for admin endpoints | +| admin-password | password | SPLIT_SYNC_ADMIN_PASSWORD | HTTP basic auth password for admin endpoints | +| admin-secure-hc | secureChecks | SPLIT_SYNC_ADMIN_SECURE_HC | Secure Healthcheck endpoints as well. | +| admin-tls-enabled | enabled | SPLIT_SYNC_ADMIN_TLS_ENABLED | Enable HTTPS on proxy endpoints. | +| admin-tls-client-validation | clientValidation | SPLIT_SYNC_ADMIN_TLS_CLIENT_VALIDATION | Enable client cert validation. | +| admin-tls-server-name | serverName | SPLIT_SYNC_ADMIN_TLS_SERVER_NAME | Server name as it appears in provided server-cert. | +| admin-tls-cert-chain-fn | certChainFn | SPLIT_SYNC_ADMIN_TLS_CERT_CHAIN_FN | X509 Server certificate chain. | +| admin-tls-private-key-fn | privateKeyFn | SPLIT_SYNC_ADMIN_TLS_PRIVATE_KEY_FN | PEM Private key file name. | +| admin-tls-client-validation-root-cert | clientValidationRootCertFn | SPLIT_SYNC_ADMIN_TLS_CLIENT_VALIDATION_ROOT_CERT | X509 root cert for client validation. | +| admin-tls-min-tls-version | minTlsVersion | SPLIT_SYNC_ADMIN_TLS_MIN_TLS_VERSION | Minimum TLS version to allow X.Y. | +| admin-tls-allowed-cipher-suites | allowedCipherSuites | SPLIT_SYNC_ADMIN_TLS_ALLOWED_CIPHER_SUITES | Comma-separated list of cipher suites to allow. | +| impression-listener-endpoint | endpoint | SPLIT_SYNC_IMPRESSION_LISTENER_ENDPOINT | HTTP endpoint to forward impressions to | +| impression-listener-queue-size | queueSize | SPLIT_SYNC_IMPRESSION_LISTENER_QUEUE_SIZE | max number of impressions bulks to queue | +| slack-webhook | webhook | SPLIT_SYNC_SLACK_WEBHOOK | slack webhook to post log messages | +| slack-channel | channel | SPLIT_SYNC_SLACK_CHANNEL | slack channel to post log messages | +| apikey | apikey | SPLIT_SYNC_APIKEY | Split Server-side SDK api-key | +| ip-address-enabled | ipAddressEnabled | SPLIT_SYNC_IP_ADDRESS_ENABLED | Bundle host's ip address when sending data to Split | +| timeout-ms | timeoutMS | SPLIT_SYNC_TIMEOUT_MS | How long to wait until the synchronizer is ready | +| snapshot | snapshot | SPLIT_SYNC_SNAPSHOT | Snapshot file to use as a starting point | +| force-fresh-startup | forceFreshStartup | SPLIT_SYNC_FORCE_FRESH_STARTUP | Wipe storage before starting the synchronizer | +| storage-type | type | SPLIT_SYNC_STORAGE_TYPE | Storage driver to use for caching feature flags and segments and user-generated data | +| split-refresh-rate-ms | splitRefreshRateMs | SPLIT_SYNC_SPLIT_REFRESH_RATE_MS | How often to refresh feature flags | +| segment-refresh-rate-ms | segmentRefreshRateMs | SPLIT_SYNC_SEGMENT_REFRESH_RATE_MS | How often to refresh segments | +| impressions-mode | impressionsMode | SPLIT_SYNC_IMPRESSIONS_MODE | whether to send all impressions for debugging | +| streaming-enabled | streamingEnabled | SPLIT_SYNC_STREAMING_ENABLED | Enable/disable streaming functionality | +| http-timeout-ms | httpTimeoutMs | SPLIT_SYNC_HTTP_TIMEOUT_MS | Total http request timeout | +| internal-metrics-rate-ms | internalTelemetryRateMs | SPLIT_SYNC_INTERNAL_METRICS_RATE_MS | How often to send internal metrics | +| telemetry-push-rate-ms | telemetryPushRateMs | SPLIT_SYNC_TELEMETRY_PUSH_RATE_MS | how often to flush sdk telemetry | +| impressions-fetch-size | impressionsFetchSize | SPLIT_SYNC_IMPRESSIONS_FETCH_SIZE | Impression fetch bulk size | +| impressions-process-concurrency | impressionsProcessConcurrency | SPLIT_SYNC_IMPRESSIONS_PROCESS_CONCURRENCY | #Threads for processing imps | +| impressions-process-batch-size | impressionsProcessBatchSize | SPLIT_SYNC_IMPRESSIONS_PROCESS_BATCH_SIZE | Size of imp processing batchs | +| impressions-post-concurrency | impressionsPostConcurrency | SPLIT_SYNC_IMPRESSIONS_POST_CONCURRENCY | #concurrent imp post threads | +| impressions-post-size | impressionsPostSize | SPLIT_SYNC_IMPRESSIONS_POST_SIZE | Max #impressions to send per POST | +| impressions-accum-wait-ms | impressionsAccumWaitMs | SPLIT_SYNC_IMPRESSIONS_ACCUM_WAIT_MS | Max ms to wait to close an impressions bulk | +| events-fetch-size | eventsFetchSize | SPLIT_SYNC_EVENTS_FETCH_SIZE | How many impressions to pop from storage at once | +| events-process-concurrency | eventsProcessConcurrency | SPLIT_SYNC_EVENTS_PROCESS_CONCURRENCY | #Threads for processing imps | +| events-process-batch-size | eventsProcessBatchSize | SPLIT_SYNC_EVENTS_PROCESS_BATCH_SIZE | Size of imp processing batchs | +| events-post-concurrency | eventsPostConcurrency | SPLIT_SYNC_EVENTS_POST_CONCURRENCY | #concurrent imp post threads | +| events-post-size | eventsPostSize | SPLIT_SYNC_EVENTS_POST_SIZE | Max #impressions to send per POST | +| events-accum-wait-ms | eventsAccumWaitMs | SPLIT_SYNC_EVENTS_ACCUM_WAIT_MS | Max ms to wait to close an events bulk | +| redis-host | host | SPLIT_SYNC_REDIS_HOST | Redis server hostname | +| redis-port | port | SPLIT_SYNC_REDIS_PORT | Redis Server port | +| redis-db | db | SPLIT_SYNC_REDIS_DB | Redis DB | +| redis-pass | password | SPLIT_SYNC_REDIS_PASS | Redis password | +| redis-user | username | SPLIT_SYNC_REDIS_USER | Redis username | +| redis-prefix | prefix | SPLIT_SYNC_REDIS_PREFIX | Redis key prefix | +| redis-network | network | SPLIT_SYNC_REDIS_NETWORK | Redis network protocol | +| redis-max-retries | maxRetries | SPLIT_SYNC_REDIS_MAX_RETRIES | Redis connection max retries | +| redis-dial-timeout | dialTimeout | SPLIT_SYNC_REDIS_DIAL_TIMEOUT | Redis connection dial timeout | +| redis-read-timeout | readTimeout | SPLIT_SYNC_REDIS_READ_TIMEOUT | Redis connection read timeout | +| redis-write-timeout | writeTimeout | SPLIT_SYNC_REDIS_WRITE_TIMEOUT | Redis connection write timeout | +| redis-pool | poolSize | SPLIT_SYNC_REDIS_POOL | Redis connection pool size | +| redis-sentinel-replication | sentinelReplication | SPLIT_SYNC_REDIS_SENTINEL_REPLICATION | Redis sentinel replication enabled. | +| redis-sentinel-addresses | sentinelAddresses | SPLIT_SYNC_REDIS_SENTINEL_ADDRESSES | List of redis sentinels | +| redis-sentinel-master | sentinelMaster | SPLIT_SYNC_REDIS_SENTINEL_MASTER | Name of master | +| redis-cluster-mode | clusterMode | SPLIT_SYNC_REDIS_CLUSTER_MODE | Redis cluster enabled. | +| redis-cluster-nodes | clusterNodes | SPLIT_SYNC_REDIS_CLUSTER_NODES | List of redis cluster nodes. | +| redis-cluster-key-hashtag | keyHashTag | SPLIT_SYNC_REDIS_CLUSTER_KEY_HASHTAG | keyHashTag for redis cluster. | +| redis-tls | enableTLS | SPLIT_SYNC_REDIS_TLS | Use SSL/TLS for connecting to redis | +| redis-tls-server-name | tlsServerName | SPLIT_SYNC_REDIS_TLS_SERVER_NAME | Server name to use when validating a server public key | +| redis-tls-ca-certs | caCertificates | SPLIT_SYNC_REDIS_TLS_CA_CERTS | Root CA certificates to connect to a redis server via SSL/TLS | +| redis-tls-skip-name-validation | tlsSkipNameValidation | SPLIT_SYNC_REDIS_TLS_SKIP_NAME_VALIDATION | Blindly accept server's public key. | +| redis-tls-client-certificate | tlsClientCertificate | SPLIT_SYNC_REDIS_TLS_CLIENT_CERTIFICATE | Client certificate signed by a known CA | +| redis-tls-client-key | tlsClientKey | SPLIT_SYNC_REDIS_TLS_CLIENT_KEY | Client private key matching the certificate. | +| storage-check-rate-ms | storageCheckRateMs | SPLIT_SYNC_STORAGE_CHECK_RATE_MS | How often to check storage health | +| flag-sets-filter | flagSetsFilter | SPLIT_SYNC_FLAG_SETS_FILTER | This setting allows the split synchronizer to only synchronize the feature flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the split synchronizer instance, bringing all the benefits from a reduced payload. | + +## Listener + +The Split Synchronizer provides an impression listener that bulks post impressions to a user-defined HTTP endpoint. + +The endpoint should expect a POST request, containing a JSON body with the following format. + +```json title="JSON Impression" +{ + "impressions": [ + { + "testName": "feature1", + "keyImpressions": [ + { + "keyName": "user1", + "treatment": "on", + "time": 1502754901182, + "changeNumber": -1, + "label": "" + }, + { + "keyName": "user2", + "treatment": "off", + "time": 1502754876144, + "changeNumber": -1, + "label": "" + } + ] + } + ], + "sdkVersion": "php-5.2.2", + "machineIP": "208.63.222.7", + "MachineName": "" +} +``` + +Currently, the configuration options are available in the `integrations.impressionListener` section of the JSON configuration file detailed in [Advanced configuration](#advanced-configuration). + +## Using a network proxy + +If you need to use a network proxy, configure proxies by setting the environment variables **HTTP_PROXY** and **HTTPS_PROXY**. The internal HTTP client reads those variables and uses them to perform the server request. + +```bash title="Example: Environment variables" +$ export HTTP_PROXY="http://10.10.1.10:3128" +$ export HTTPS_PROXY="http://10.10.1.10:1080" +``` + +## Admin tools + +### Endpoints + +The `split-sync` service has a set of endpoints and a dashboard to let DevOps and infra team monitor its status and cached data in real-time. By default the port is `3010` and for security reason supports HTTP Basic Authentication configured by the user. + +**/info/ping** + +A ping endpoint to monitor the service status. If the service is running, it sends the text response `pong` and the HTTP status code `200`. + +**/info/version** + +Returns the `split-sync` version in JSON format. + +```json +{ + "version" : "1.1.0" +} +``` + +**/info/uptime** + +Returns the uptime string representation in JSON format. + +```json +{ + "uptime" : "5d 3h 36m 39s" +} +``` + +**/info/config** +Returns a JSON object describing the current configuration of the Synchronizer. + +```json +{ + "config": { + "apikey": "*", + "ipAddressEnabled": true, + "initialization": { + "timeoutMS": 10000, + "forceFreshStartup": false + }, + "storage": { + "type": "redis", + "redis": { + "host": "localhost", + "port": 6379, + "db": 0, + "username": "", + "password": "", + "prefix": "", + "network": "tcp", + "maxRetries": 0, + "dialTimeout": 5, + "readTimeout": 10, + "writeTimeout": 5, + "poolSize": 10, + "sentinelReplication": false, + "sentinelAddresses": "", + "sentinelMaster": "", + "clusterMode": false, + "clusterNodes": "", + "keyHashTag": "", + "enableTLS": false, + "tlsServerName": "", + "caCertificates": null, + "tlsSkipNameValidation": false, + "tlsClientCertificate": "", + "tlsClientKey": "" + } + }, + "sync": { + "splitRefreshRateMs": 60000, + "segmentRefreshRateMs": 60000, + "impressionsMode": "optimized", + "advanced": { + "streamingEnabled": true, + "httpTimeoutMs": 30000, + "internalTelemetryRateMs": 3600000, + "telemetryPushRateMs": 60000, + "impressionsFetchSize": 0, + "impressionsProcessConcurrency": 0, + "impressionsProcessBatchSize": 0, + "impressionsPostConcurrency": 0, + "impressionsPostSize": 0, + "impressionsAccumWaitMs": 0, + "eventsFetchSize": 0, + "eventsProcessConcurrency": 0, + "eventsProcessBatchSize": 0, + "eventsPostConcurrency": 0, + "eventsPostSize": 0, + "eventsAccumWaitMs": 0 + } + }, + "admin": { + "host": "0.0.0.0", + "port": 3010, + "username": "", + "password": "", + "secureChecks": false + }, + "integrations": { + "impressionListener": { + "endpoint": "", + "queueSize": 100 + }, + "slack": { + "webhook": "", + "channel": "" + } + }, + "logging": { + "level": "info", + "output": "stdout", + "rotationMaxFiles": 10, + "rotationMaxSizeKb": 0 + }, + "healthcheck": { + "app": { + "storageCheckRateMs": 3600000 + } + } + } +} +``` + +**/health/application** +Returns a JSON object describing whether the synchronizer is healthy or not. + +```json +{ + "healthy": true, + "healthySince": "2021-11-20T19:04:46.528242-03:00", + "items": [ + { + "name": "Splits", + "healthy": true, + "lastHit": "2021-11-20T19:04:49.079956-03:00" + }, + { + "name": "Segments", + "healthy": true, + "lastHit": "2021-11-20T19:04:49.268349-03:00" + }, + { + "name": "Storage", + "healthy": true + } + ] +} + +``` + +**/health/dependencies** +Returns a JSON object describing whether the servers the synchronizer depends on are healthy or not. + +```json +{ + "serviceStatus": "healthy", + "dependencies": [ + { + "service": "https://telemetry.split.io/health", + "healthy": true, + "healthySince": "2021-11-20T19:04:46.528262-03:00" + }, + { + "service": "https://auth.split.io/health", + "healthy": true, + "healthySince": "2021-11-20T19:04:46.528264-03:00" + }, + { + "service": "https://sdk.split.io/api/version", + "healthy": true, + "healthySince": "2021-11-20T19:04:46.528265-03:00" + }, + { + "service": "https://events.split.io/api/version", + "healthy": true, + "healthySince": "2021-11-20T19:04:46.528266-03:00" + }, + { + "service": "https://streaming.split.io/health", + "healthy": true, + "healthySince": "2021-11-20T19:04:46.528266-03:00" + } + ] +} +``` + +### Admin Dashboard + +Split-sync has a web admin UI out of the box that exposes all available endpoints. Browse to `/admin/dashboard` to see it. + +

+ split_synchronizer_dashboard_main.png +

+ +

+ split_synchronizer_dashboard_stats.png +

+ +The dashboard is organized in four sections for ease of visualization: + +* **Dashboard:** Tile-sorted summary information, including these metrics: + - *Uptime:* Uptime metric + - *Healthy Since:* Time passed without errors + - *Logged Errors:* Total count of error messages + - *SDKs Total Hits:* Total SDKs requests + - *Backend Total Hits:* Total backend requests between split-sync and Split servers + - *Cached Feature flags:* Number of feature flags cached in memory + - *Cached Segments:* Number of segments cached in memory + - *Impressions Queue Size*: shows the total amount of Impressions stored in Redis (only Producer Mode). + - *Impressions Lambda*: shows the eviction rate for Impressions (only Producer Mode). + - *Events Queue Size*: shows the total amount of Events stored in Redis (only Producer Mode). + - *Events Lambda*: shows the eviction rate for Events (only Producer Mode). + - *SDK Server*: displays the status of Split server for SDK. + - *Events Server*: displays the status of Split server for Events. + - Streaming Server*: displays the status of Split streaming service + - Auth Server*: displays the status of Split server for initial streaming authentication + - Telemetry Server*: displays the status of Split server for telemetry capturing. + - *Storage*: (only Sync mode) displays the status of the storage. + - *Sync*: displays the status of the Synchronizer. + - *Last Errors Log:* List of the last 10 error messages +* **SDK stats:** Metrics numbers and a latency graph, measured between SDKs requests integration and proxy. +* **Data inspector:** Cached data showing feature flags and segments; filters to find keys and feature flag definitions. +* **Queue Manager:** expose sizes of Impressions and Events queues. +

+ split_synchronizer_dashboard_queue_manager.png +

+ +:::warning[Dashboard refresh rate] +The dashboard numbers are committed every 60 seconds. The `Logged Errors`, `Last Errors Log` tiles, and the `Data inspector` section are populated each time the dashboard is refreshed. +For Impressions and Events Queue size the numbers are refreshed every 10 seconds. +::: + +### Service shutdown + +The `split-sync` service can catch a `kill sig` command and start a graceful shutdown, flushing all cached data progressively. Additionally, you can perform `graceful stop` and `force stop (kill -9)` with one click from the admin dashboard. + +

+ split_synchronizer_stop_button.png +

+ +If you have configured a Slack channel and the Slack Webhook URL, an alert is sent to the channel when and initialization or shutdown is performed. + +

+ split_synchronizer_slack.png +

\ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/sdk-overview.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/sdk-overview.md new file mode 100644 index 00000000000..69a298ec67f --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/sdk-overview.md @@ -0,0 +1,145 @@ +--- +title: SDK overview +sidebar_label: SDK overview +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 1 +--- + +

+ +

+ +When you integrate Split SDKs, consider the following to make sure that you have the correct set up depending on your use case, customers, security considerations, and architecture. + +* **Understand Split's architecture**. Split's SDKs were built to be scalable, reliable, fast, independent, and secure. +* **Determine which SDK type**. Depending on your use case and your application stack, you may need a server-side or client-side SDK. +* **Understand security considerations**. Client- and server-side SDKs have different security considerations when managing and targeting using your customers' PII. +* **Determine which API key**. In Split, there are three types of keys with each providing different levels of access to Split's API. Understand what each key provides access to and when to use each API key. +* **Determine which SDK language**. Split supports serveral SDKs across various languages. With Split, you can use multiple SDKs if your product is comprised of applications written in multiple languages. +* **Determine if you need to use the Split Synchronizer & Proxy**. By default, Split's SDKs keep segment and feature flag definitions synchronized as users navigate across disparate systems, treatments, and conditions. However, some languages do not have a native capability to keep a shared local cache of this data to properly serve treatments. For these cases, we built the Split Synchronizer. To learn more, refer to the [Split Synchronizer and Proxy guide](https://help.split.io/hc/en-us/articles/360019686092-Split-synchronizer-proxy). + +## Streaming architecture overview + +Split's SDKs were built to be scalable, reliable, fast, independent, and secure. + +* **Scalable**. Split is currently serving more than 50 billion Split feature flag evaluations per day. If you've shopped online, purchased an airline ticket, or received a text message from service provider, you've likely experienced Split. +* **Reliable and fast**. Our scalable and flexible architecture uses a dual-layer CDN to serve feature flags anywhere in the world in less than 200 ms. In most instances, Split rollout plan updates are streamed to Split's SDKs, which takes a fraction of a second. In less than 10% of cases, for very large feature flag definitions (or large dynamic configs) or segment updates with a large number of key changes, a notification of the change is streamed and the changes are retrieved by an API fetch request. Our SDKs store the Split rollout plan locally to serve feature flags without a network call and without interruption in the event of a network outage. +* **Independent with no Split dependency**. Split ships the evaluation engine to each SDK creating a weak dependency with Split's backend and increasing both speed and reliability. There are no network calls to Split to decide a user's treatment. +* **Secure with no PII required**. No customer data needs to be sent through the cloud to Split. Use customer data in your feature flag evaluations without exposing this data to third parties. + +## Streaming versus polling + +Split updates can be streamed to Split's SDKs sub second or retrieved on configurable polling intervals. + +When streaming, Split utilizes [server-sent events (SSE)](https://www.w3schools.com/html/html5_serversentevents.asp) to notify Split’s SDKs when a feature flag definition is updated, a segment definition is updated, or a feature flag is killed. For feature flag and segment definition updates, the Split SDK reacts to this notification and fetches the latest feature flag definition or segment definition. When a feature flag is killed, the notification triggers a kill event immediately. When the SDK is running with streaming enabled, your updates take effect in milliseconds. + +Enable streaming when it is important to: + +* Reduce network traffic caused by frequent polling +* Propagate split updates to every customer and/or service in real-time + +When polling, the SDK asks the server for updates on configurable polling intervals. Each request is optimized to fetch delta changes resulting in small payload sizes. + +Utilize polling when it is important to: + +* Maintain a lower memory footprint. Each streaming connection is treated as an independent request +* Support environments with unreliable connectivity such as mobile networks. Mobile environments benefit from a low-frequency polling architecture +* Maintain robust security practices. Maintaining an always-open streaming connection poses risk +* Maintain control over frequency and when to initiate a network call + +:::warning[Streaming is currently supported for the below SDKs with the minimum version shown below.] + +* .NET 6.1.0 +* Android 2.6.0 +* Browser 0.1.0 +* Go 5.2.0 +* iOS 2.7.0 +* Java 4.0.0 +* JavaScript 10.12.0 +* Node.js 10.12.0 +* React 1.2.0 +* React Native 0.0.1 +* Redux 1.2.0 +* Ruby 7.1.0 +* Python: 8.3.0 +::: + +## SDK types + +Our supported SDKs fall into two categories: + +| **Type** | **Overview** | +| --- | --- | +| Client-side |
  • Designed to be used by a single traffic type in the browser, mobile device, or mobile application
  • Intended to be used in a potentially less secure environment
  • This includes Split's JavaScript, iOS, and Android SDKs
| +| Server-side |
  • Designed to work for multiple traffic types, like users or customers (many of them per SDK) as opposed to client-side that are bound to one (typically a single user or account in session)
  • Intended to be used in a secure environment, such as your infrastructure
| + +## Security considerations + +Client- and server-side SDKs have different security considerations: + +| **Type** | **Security Considerations** | +| --- | --- | +| Client-side |
  • These SDKs run on the browser or in a mobile device, they can be compromised by users unpacking a mobile app or use the browser's developer tools to inspect the page
  • Client-side SDK APIs are more restricted in regards to what information they can access because it's a less secure environment
    For example, client-side SDKs uses a specific endpoint (/mySegments) which only returns a list of segments in which the key used during instantiation is included. This provides for a much smaller amount of data, allowing for a smaller memory footprint in memory constrained environments of the browser and mobile apps
| +| Server-side |
  • These SDKs operate within your own infrastructure making them not accessible by end users
  • When targeting by private or sensitive data on the server-side, this information won't leave your infrastructure, keeping your sensitive data under your control
| + +## API keys + +Typically, you need one API key per Split environment, and additionally, you may want to issue extra API keys per microservice of your product using Split for better security isolation. You must identify which type of SDK you're using to ensure you select the appropriate API key type. + +Within Split, the following three types of keys each provide different levels of access to Split's API: + +| **Type** | **Overview** | +| --- | --- | +| Server-side |
  • Configure server-side SDKs to use a server-side api key
  • Grants access to fetch feature flags and segments associated within the provided API key's environment
  • Never expose server-side keys in untrusted contexts
  • Do not put your server-side API keys in client-side SDKs
  • If you accidentally expose your server-side API key, you can revoke it in the API keys tab in Admin settings
| +| Client-side |
  • Configure client-side SDKs to use the client-side api key
  • Grants access to fetch featuer flags and segments for the provided key within the provided API key's environment
| +| Admin |
  • Use for access to Split's developer admin API
  • This key provides broader access to multiple environments unlike the other API keys that are scoped to a specific environment
  • Do not share this API key with your customers
  • If you accidentally expose your admin API key, you can revoke it in the API keys tab in Admin settings
| + +## Supported SDKs + +Using Split involves using one of our SDKs. The Split team builds and maintains these SDKs for some of the most popular language libraries and are available under open source licenses. Go to our GitHub repository for more information. + +| **SDK** | **API Key/Type** | **Links** | +| --- | --- | --- | +| Android | client-side | [Docs](https://help.split.io/hc/en-us/articles/360020343291-Android-SDK) & [GitHub](https://github.com/splitio/android-client) | +| Angular utilities | client-side | [Docs](https://help.split.io/hc/en-us/articles/6495326064397-Angular-utilities) & [GitHub](https://github.com/splitio/angular-sdk-plugin) | +| Browser | client-side | [Docs](https://help.split.io/hc/en-us/articles/360058730852-Browser-SDK) & [GitHub](https://github.com/splitio/javascript-browser-client) | +| Flutter plugin | client-side | [Docs](https://help.split.io/hc/en-us/articles/8096158017165-Flutter-plugin) & [GitHub](https://github.com/splitio/flutter-sdk-plugin) | +| iOS | client-side | [Docs](https://help.split.io/hc/en-us/articles/360020401491-iOS-SDK) & [GitHub](https://github.com/splitio/ios-client) | +| JavaScript | client-side | [Docs](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK) & [GitHub](https://github.com/splitio/javascript-client) | +| React | client-side | [Docs](https://help.split.io/hc/en-us/articles/360038825091) & [GitHub](https://github.com/splitio/react-client) | +| React Native | client-side | [Docs](https://help.split.io/hc/en-us/articles/4406066357901-React-Native-SDK) & [GitHub](https://github.com/splitio/react-native-client) | +| Redux | client-side | [Docs](https://help.split.io/hc/en-us/articles/360038851551) & [GitHub](https://github.com/splitio/redux-client) | +| Elixir Thin-Client | server-side | [Docs](https://help.split.io/hc/en-us/articles/26988707417869-Elixir-Thin-Client-SDK) & [GitHub](https://github.com/splitio/elixir-thin-client) | +| GO | server-side | [Docs](https://help.split.io/hc/en-us/articles/360020093652-Go-SDK) & [GitHub](https://github.com/splitio/go-client) | +| Java | server-side | [Docs](https://help.split.io/hc/en-us/articles/360020405151-Java-SDK) & [GitHub](https://github.com/splitio/java-client) | +| .NET | server-side | [Docs](https://help.split.io/hc/en-us/articles/360020240172--NET-SDK) & [GitHub](https://github.com/splitio/.net-core-client) | +| Node.js | server-side | [Docs](https://help.split.io/hc/en-us/articles/360020564931-Node-js-SDK) & [GitHub](https://github.com/splitio/javascript-client) | +| PHP | server-side | [Docs](https://help.split.io/hc/en-us/articles/360020350372-PHP-SDK) & [GitHub](https://github.com/splitio/php-client) | +| PHP Thin-Client | server-side | [Docs](https://help.split.io/hc/en-us/articles/18305128673933-PHP-Thin-Client-SDK) & [GitHub](https://github.com/splitio/php-thin-client) | +| Python | server-side | [Docs](https://help.split.io/hc/en-us/articles/360020359652-Python-SDK) & [GitHub](https://github.com/splitio/python-client) | +| Ruby | server-side | [Docs](https://help.split.io/hc/en-us/articles/360020673251-Ruby-SDK) & [GitHub](https://github.com/splitio/ruby-client) | + +## Evaluator service + +For languages with no native SDK support, Split offers the Split Evaluator, a small service capable of evaluating all available features for a given customer via a REST endpoint. This service is available as a Docker container for ease of installation and is compatible with popular framework like Kubernetes when it comes to supporting standard health checks to achieve reliable uptimes. Learn more about the [Split evaluator](https://help.split.io/hc/en-us/articles/360020037072-Split-Evaluator). + +## Synchronizer service + +By default, Split's SDKs keep segment and feature flag definitions synchronized in an in-memory cache for speed at evaluating feature flags. However, some languages do not have a native capability to keep a shared local cache of this data to properly serve treatments. For these cases, we built Split Synchronizer to maintain an external cache like Redis. To learn more, read about [Split Synchronizer](https://help.split.io/hc/en-us/articles/360019686092-Split-Synchronizer). + +## Proxy service + +Split Proxy enables you to deploy a service in your own infrastructure that behaves like Split's servers and is used by both server-side and client-side SDKs to synchronize the flags without directly connecting to Split's backend. + +This tool reduces connection latencies between the SDKs and the Split server, and can be used when a single connection is required from a private network to the outside for security reasons. To learn more, read about [Split Proxy](https://help.split.io/hc/en-us/articles/4415960499213-Split-Proxy). + +## Supported agents + +Split's real user monitoring (RUM) agents collect detailed information about your users' experience when they visit your application. This information is used to analyze site impact, measure the degradation of performance metrics in relation to feature flag changes and alert the owner of the feature flag about such degradation. + +| **Agent** | **API Key/Type** | **Docs** | +| --- | --- | --- | +| Android | client-side | [Docs](https://help.split.io/hc/en-us/articles/18530305949837-Android-RUM-Agent) | +| iOS | client-side | [Docs](https://help.split.io/hc/en-us/articles/22545155055373-iOS-RUM-Agent) | +| Browser | client-side | [Docs](https://help.split.io/hc/en-us/articles/360030898431-Browser-RUM-Agent) | \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/sdk-validation-checklist.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/sdk-validation-checklist.md new file mode 100644 index 00000000000..3657b0af2f9 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/sdk-validation-checklist.md @@ -0,0 +1,93 @@ +--- +title: SDK validation checklist +sidebar_label: SDK validation checklist +sidebar_position: 2 +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +The SDK validation checklist helps you ensure that the SDK is implemented according to Split’s best practices. This document describes the guidelines for incorporating the Split SDK into your software application in all supported languages. The main purpose is to define the general guidelines, checks, and validations that can be useful for developers and software architects to avoid common mistakes or oversights and to ensure optimal performance of the Split SDK. This guide covers recommendations in the following areas: + +* Architectural design principles +* Safety checks for prevention of race conditions +* Taking advantage of helpful Split features +* Configuration validation exercises + +These areas each reflect best practices that come from our own experience at Split using the Split SDK, and the experiences of customers like you. In addition, they also convey an understanding of how Split SDK works beneath the surface. + +You can use or adapt them to your needs. The primary objectives are to ensure resource optimization, maximum application responsiveness, appropriate security enforcements, and proactive issue detection in your project, team, organization, or company source code working with the Split SDK. + +## All SDKs + +The following validation considerations are relevant for all of Split’s SDKs. + +* **Ensure that the SDK is implemented in a singleton pattern.** Using the SDK as a singleton ensures that the minimum number of threads are used to serve your application. If you don’t, you can overload your infrastructure with unnecessary network traffic and use up far more application threads than is required. Use multiple clients on the client side from a single factory if you need to get treatments for multiple different traffic type ids. + +* **Ensure that the SDK is blocked until it signals it’s ready.** All Split SDKs have a method that blocks the thread until the SDK is ready with feature flag and segment definitions. Calling getTreatment before the SDK is ready gives CONTROL treatments. + +* **Run the SDK with DEBUG enabled and evaluate any errors or warning messages that are thrown.** Pay attention specifically to errors or warnings related to multiple factories, missing event listeners, or other incorrect factory and client configuration. **Note: Run with debug enabled for only a few minutes.** + +* **Ensure any code calling getTreatment is able to handle when ‘CONTROL’ is returned.** The SDKs return ‘CONTROL’ as a treatment string when there is a connectivity error. Ensure that there is a fall through in the if-statement to support this. + +* **Validate SDK Versions are up to date.** Review the SDK tab on the Account usage data page. Ensure that the SDKs are up to date, or, at the minimum, they are on the same major version. It is helpful to establish and document a regular SDK update cadence, such as quarterly or biannually. Check the SDK CHANGES.txt on github for any SDKs you are using to see if anything may be relevant to your usage of Split. + +* **Evaluate if you can take advantage of the SDK .destroy() method.** The .destroy() method of the SDK flushes all stored unpublished events and impressions. This is primarily advantageous for the client side SDKs where you have parts of the user journey that explicitly end their session. On the browser, .destroy() returns a promise. If it’s resolved, then you can be sure that all data is pushed to Split. On the server side it also may be useful in the event that you need to shutdown a service running the Split SDK. Calling .destroy() ensures that data is posted back to Split. + +* **Validate 1 minute of impressions (and events) on the Split live tail.** Enable the Query for about a minute and ensure that the number of impressions received by Split is about what you’d expect from SDK activity. + +* **If you have events coming in, validate them with a similar approach.** Ensure that events coming in have the event properties that you would expect them to have. + +* **Validate that all expected attributes are being passed to the SDK.** Split’s recommendation is to wrap the SDK to ensure that attributes are always passed to the SDK. A consistent attribute set is important to ensure that all targeting rules have access to the same list of attributes. Client Side SDKs also have the ability to bind attributes to the client itself. + +* **Validate 24 hours of impressions (and events) from the Data hub.** Take a feature flag that has a known high activity and download all impressions for it from the previous 24 hour period. Ensure that the number of treatments and IDs all match with expectations. For events, take the previous full 24 hours of events, if applicable. With impressions, take note if you are seeing any ‘CONTROL’ treatments as those warrant further investigation to understand why those are happening. + +* **Evaluate if you can take advantage of Flag Sets** You can use Split Flag sets for limiting the flags downloaded by an SDK. [Flag Sets](https://help.split.io/hc/en-us/articles/22256278916621-Using-flag-sets-to-boost-SDK-performance) allow you to control from Split's UI which flags are downloaded by an SDK. This means you can ensure that only the flags needed for a frontend SDK or a backend SDK are downloaded. This reduces the time for the SDK to get ready while also saving memory and bandwidth. + +## Browser SDKs (including Angular, React, etc.) + +The following items are specific to browser-based SDKs. + +* **(React-specific) Ensure that the SDK is only used in a component or higher-order component (HOC).** Review the code samples on our help center. Do not create a new factory for each time a subcomponent is rendered. + +* **Evaluate if localStorage mode is something you may be interested in.** By default, the SDK stores the cache in memory, which means every time the user visits the page, the SDK has to re-download the whole cache again. + + Using this option stores the cache in the browser file system, which improves the SDK performance after the first load. For more information, refer to [Why does the JavaScript SDK return Not Ready status in slow networks?](https://help.split.io/hc/en-us/articles/360012551371-Why-does-the-Javascript-SDK-return-Not-Ready-status-in-Slow-Networks-) Using this option also allows users to view localStorage in their browser to see rollout plans. If you are use multiple factories, ensure that you are setting prefixes explicitly so they don’t overwrite one another’s localstorage objects. + +* **Evaluate if you can take advantage of lazy loading.** The SDK factory must have the customer key at initialization time. This key might not be available initially though, especially if the key is provided from another tool (e.g., Segment or mParticle). Using the Lazy init allows you to initialize the SDK by passing a dummy key, then create a new client from the same factory object when the actual customer key is obtained. + +## Mobile SDKs + +The following items are specific to the mobile SDKs. + +* **Ensure that the SDK background syncing is enabled if desired.** Mobile SDKs have the synchronizeInBackground configuration setting that allows them to synchronize to the Split cloud while in the background. By default, this is disabled. + +## All Client-side SDKs (including iOS, React, JS, etc.) + +The following items are specific to all client-side SDKs. This includes mobile- and browser-based SDKs. + +* **Evaluate if you can take advantage of additional SDK emitted events.** In addition to SDK_READY, client side SDKs also emit the following additional events that may be useful: + * SDK_READY_FROM_CACHE. The SDK is ready to evaluate using cached data (which might be stale). If conditions are met, this event is emitted almost immediately since access to the cache is synchronous. Otherwise it won't fire. + * SDK_READY_TIMED_OUT. When this event fires, it doesn't mean the SDK initialization is interrupted. SDK_READY may still fire at a later time if or when the SDK finishes downloading the necessary information from the servers. This may happen with slow connections or environments which have many feature flags, segments, or dynamic configurations. + * SDK_UPDATE. This event fires whenever a feature flag or segment is changed. Use this if you want to reload your app every time you make a change in the user interface. + +* **Evaluate if you need to change the flush rate.** The SDK posts impressions on frequency based on the parameter scheduler.impressionsRefreshRate. By default, the parameter is set to 60 seconds in the browser and 30 minutes in the mobile SDKs. This means after the getTreatment function is called, impressions get posted back to the Split cloud after that length of time. + + On mobile devices, if the user stays in the app for less than that amount of time, the impressions stay in the SDK cache. However, they are not posted as the posting thread has not run yet. The next time a user opens the app, the impressions are posted but this can be a few days later. + + For browsers, the JS SDKs use the beacon API to post results back to the Split cloud when the page is no longer visible. + + For experimentation, it is desired to have the results up to date. It is recommended to set the parameter scheduler.impressionsRefreshRate to a value less than the average time the user stays on the app. + +## Server-side SDKs (Python, Node.js, Java, etc.) + +The following items are specific to server-side SDKs. + +* **Evaluate your traffic needs.** You may need to change the impressionsRefreshrate. The SDK has threads that sync the Split information from Split cloud to the cache, and posts all impressions and events created in the cache. Make sure the SDK can handle the incoming impressions load because the SDK drops impressions if the cap is reached in the impressionsQueue and impressions can’t be evicted. + + The SDK has parameters to control the run frequency for these threads. We recommend to estimate the highest number of impressions created at peak time from incoming user sessions and divide that by the number of app servers that have the SDK to estimate the number of treatments per minute each SDK generates. Roughly, the SDK’s default impressionsQueue can handle 2000 treatments per minute. If the peak time generates higher impressions, we can reduce the value of scheduler.impressionsRefreshRate by half (for example, from 60 to 30 seconds). + +**Note This traffic sizing is for pushing data back to Split cloud. Even if the impressionsQueue is full and drops impressions, serving treatments are not affected.** \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/sdk-versioning-policy.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/sdk-versioning-policy.md new file mode 100644 index 00000000000..9cd930d19bd --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/sdk-versioning-policy.md @@ -0,0 +1,43 @@ +--- +title: SDK versioning policy +sidebar_label: SDK versioning policy +sidebar_position: 1 +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +Split versions SDKs according to semantic versioning industry standards and actively support major versions for 12 months post release. + +## What is semantic versioning? + +Semantic versioning establishes a standard to uniquely name a particular release of an artifact. With semantic versioning, the unique label is made up by three components: `major` version number, `minor` version number, and `patch` version number which assembled and separated by a period `.`. The following summary is extracted from the specification referenced above. + +When we release a new version of our SDKs, we increment the major, minor, or patch number. Which component is incremented depends on the change introduced and are separated by periods. + +As is conventional in semantic versioning, we increment each according to the descriptions below: + + * `Major` version. When we make backwards incompatible API changes + + * `Minor` version. When we add backwards compatible new functionality + + * `Patch` version. When we make backwards compatible bug fixes + +Additional labels for pre-release candidates and build metadata are available as extensions. These vary on a per language basis. Example include `x.x.x-rcx`, `x.x.x-canary.x`, and `x.x.x.pre.rcx`. + +Learn more at [Semantic Versioning 2.0.](https://semver.org) + +## Adding new functionality + +When Split introduces new functionality, it qualifies as a minor release. If that functionality is a breaking change or represents an API compatibility change, it qualifies as a major version change. + +## Fixing a bug + +Bug fixes that preserve compatibility are released as a patch version. Depending on the invasiveness of the fix, the versioning is incremented as minor or major. + +## Version support + +Split supports and patches prior major releases for up to 12 months following the version release date. If 12 months has elapsed, support and Split’s SDK engineering team may ask you to first upgrade to the current major release before attempting to patch old versions. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/troubleshooting.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/troubleshooting.md new file mode 100644 index 00000000000..94a8d6f6fc9 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/troubleshooting.md @@ -0,0 +1,145 @@ +--- +title: Troubleshooting +sidebar_label: Troubleshooting +sidebar_position: 3 +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +When you integrate Split SDKs, consider the following to make sure that you have the correct set up depending on your use case, customers, security considerations, and architecture. + +* **Understand Split's architecture**. Split's SDKs were built to be scalable, reliable, fast, independent, and secure. +* **Determine which SDK type**. Depending on your use case and your application stack, you may need a server-side or client-side SDK. +* **Understand security considerations**. Client- and server-side SDKs have different security considerations when managing and targeting using your customers' PII. +* **Determine which API key**. In Split, there are three types of keys with each providing different levels of access to Split's API. Understand what each key provides access to and when to use each API key. +* **Determine which SDK language**. Split supports serveral SDKs across various languages. With Split, you can use multiple SDKs if your product is comprised of applications written in multiple languages. +* **Determine if you need to use the Split Synchronizer & Proxy**. By default, Split's SDKs keep segment and feature flag definitions synchronized as users navigate across disparate systems, treatments, and conditions. However, some languages do not have a native capability to keep a shared local cache of this data to properly serve treatments. For these cases, we built the Split Synchronizer. To learn more, refer to the [Split Synchronizer and Proxy guide](https://help.split.io/hc/en-us/articles/360019686092-Split-synchronizer-proxy). + +## Streaming architecture overview + +Split's SDKs were built to be scalable, reliable, fast, independent, and secure. + +* **Scalable**. Split is currently serving more than 50 billion Split feature flag evaluations per day. If you've shopped online, purchased an airline ticket, or received a text message from service provider, you've likely experienced Split. +* **Reliable and fast**. Our scalable and flexible architecture uses a dual-layer CDN to serve feature flags anywhere in the world in less than 200 ms. In most instances, Split rollout plan updates are streamed to Split's SDKs, which takes a fraction of a second. In less than 10% of cases, for very large feature flag definitions (or large dynamic configs) or segment updates with a large number of key changes, a notification of the change is streamed and the changes are retrieved by an API fetch request. Our SDKs store the Split rollout plan locally to serve feature flags without a network call and without interruption in the event of a network outage. +* **Independent with no Split dependency**. Split ships the evaluation engine to each SDK creating a weak dependency with Split's backend and increasing both speed and reliability. There are no network calls to Split to decide a user's treatment. +* **Secure with no PII required**. No customer data needs to be sent through the cloud to Split. Use customer data in your feature flag evaluations without exposing this data to third parties. + +## Streaming versus polling + +Split updates can be streamed to Split's SDKs sub second or retrieved on configurable polling intervals. + +When streaming, Split utilizes [server-sent events (SSE)](https://www.w3schools.com/html/html5_serversentevents.asp) to notify Split’s SDKs when a feature flag definition is updated, a segment definition is updated, or a feature flag is killed. For feature flag and segment definition updates, the Split SDK reacts to this notification and fetches the latest feature flag definition or segment definition. When a feature flag is killed, the notification triggers a kill event immediately. When the SDK is running with streaming enabled, your updates take effect in milliseconds. + +Enable streaming when it is important to: + +* Reduce network traffic caused by frequent polling +* Propagate split updates to every customer and/or service in real-time + +When polling, the SDK asks the server for updates on configurable polling intervals. Each request is optimized to fetch delta changes resulting in small payload sizes. + +Utilize polling when it is important to: + +* Maintain a lower memory footprint. Each streaming connection is treated as an independent request +* Support environments with unreliable connectivity such as mobile networks. Mobile environments benefit from a low-frequency polling architecture +* Maintain robust security practices. Maintaining an always-open streaming connection poses risk +* Maintain control over frequency and when to initiate a network call + +:::warning[Streaming is currently supported for the below SDKs with the minimum version shown below.] + +* .NET 6.1.0 +* Android 2.6.0 +* Browser 0.1.0 +* Go 5.2.0 +* iOS 2.7.0 +* Java 4.0.0 +* JavaScript 10.12.0 +* Node.js 10.12.0 +* React 1.2.0 +* React Native 0.0.1 +* Redux 1.2.0 +* Ruby 7.1.0 +* Python: 8.3.0 +::: + +## SDK types + +Our supported SDKs fall into two categories: + +| **Type** | **Overview** | +| --- | --- | +| Client-side |
  • Designed to be used by a single traffic type in the browser, mobile device, or mobile application
  • Intended to be used in a potentially less secure environment
  • This includes Split's JavaScript, iOS, and Android SDKs
| +| Server-side |
  • Designed to work for multiple traffic types, like users or customers (many of them per SDK) as opposed to client-side that are bound to one (typically a single user or account in session)
  • Intended to be used in a secure environment, such as your infrastructure
| + +## Security considerations + +Client- and server-side SDKs have different security considerations: + +| **Type** | **Security Considerations** | +| --- | --- | +| Client-side |
  • These SDKs run on the browser or in a mobile device, they can be compromised by users unpacking a mobile app or use the browser's developer tools to inspect the page
  • Client-side SDK APIs are more restricted in regards to what information they can access because it's a less secure environment
    For example, client-side SDKs uses a specific endpoint (/mySegments) which only returns a list of segments in which the key used during instantiation is included. This provides for a much smaller amount of data, allowing for a smaller memory footprint in memory constrained environments of the browser and mobile apps
| +| Server-side |
  • These SDKs operate within your own infrastructure making them not accessible by end users
  • When targeting by private or sensitive data on the server-side, this information won't leave your infrastructure, keeping your sensitive data under your control
| + +## API keys + +Typically, you need one API key per Split environment, and additionally, you may want to issue extra API keys per microservice of your product using Split for better security isolation. You must identify which type of SDK you're using to ensure you select the appropriate API key type. + +Within Split, the following three types of keys each provide different levels of access to Split's API: + +| **Type** | **Overview** | +| --- | --- | +| Server-side |
  • Configure server-side SDKs to use a server-side api key
  • Grants access to fetch feature flags and segments associated within the provided API key's environment
  • Never expose server-side keys in untrusted contexts
  • Do not put your server-side API keys in client-side SDKs
  • If you accidentally expose your server-side API key, you can revoke it in the API keys tab in Admin settings
| +| Client-side |
  • Configure client-side SDKs to use the client-side api key
  • Grants access to fetch featuer flags and segments for the provided key within the provided API key's environment
| +| Admin |
  • Use for access to Split's developer admin API
  • This key provides broader access to multiple environments unlike the other API keys that are scoped to a specific environment
  • Do not share this API key with your customers
  • If you accidentally expose your admin API key, you can revoke it in the API keys tab in Admin settings
| + +## Supported SDKs + +Using Split involves using one of our SDKs. The Split team builds and maintains these SDKs for some of the most popular language libraries and are available under open source licenses. Go to our GitHub repository for more information. + +| **SDK** | **API Key/Type** | **Links** | +| --- | --- | --- | +| Android | client-side | [Docs](https://help.split.io/hc/en-us/articles/360020343291-Android-SDK) & [GitHub](https://github.com/splitio/android-client) | +| Angular utilities | client-side | [Docs](https://help.split.io/hc/en-us/articles/6495326064397-Angular-utilities) & [GitHub](https://github.com/splitio/angular-sdk-plugin) | +| Browser | client-side | [Docs](https://help.split.io/hc/en-us/articles/360058730852-Browser-SDK) & [GitHub](https://github.com/splitio/javascript-browser-client) | +| Flutter plugin | client-side | [Docs](https://help.split.io/hc/en-us/articles/8096158017165-Flutter-plugin) & [GitHub](https://github.com/splitio/flutter-sdk-plugin) | +| iOS | client-side | [Docs](https://help.split.io/hc/en-us/articles/360020401491-iOS-SDK) & [GitHub](https://github.com/splitio/ios-client) | +| JavaScript | client-side | [Docs](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK) & [GitHub](https://github.com/splitio/javascript-client) | +| React | client-side | [Docs](https://help.split.io/hc/en-us/articles/360038825091) & [GitHub](https://github.com/splitio/react-client) | +| React Native | client-side | [Docs](https://help.split.io/hc/en-us/articles/4406066357901-React-Native-SDK) & [GitHub](https://github.com/splitio/react-native-client) | +| Redux | client-side | [Docs](https://help.split.io/hc/en-us/articles/360038851551) & [GitHub](https://github.com/splitio/redux-client) | +| Elixir Thin-Client | server-side | [Docs](https://help.split.io/hc/en-us/articles/26988707417869-Elixir-Thin-Client-SDK) & [GitHub](https://github.com/splitio/elixir-thin-client) | +| GO | server-side | [Docs](https://help.split.io/hc/en-us/articles/360020093652-Go-SDK) & [GitHub](https://github.com/splitio/go-client) | +| Java | server-side | [Docs](https://help.split.io/hc/en-us/articles/360020405151-Java-SDK) & [GitHub](https://github.com/splitio/java-client) | +| .NET | server-side | [Docs](https://help.split.io/hc/en-us/articles/360020240172--NET-SDK) & [GitHub](https://github.com/splitio/.net-core-client) | +| Node.js | server-side | [Docs](https://help.split.io/hc/en-us/articles/360020564931-Node-js-SDK) & [GitHub](https://github.com/splitio/javascript-client) | +| PHP | server-side | [Docs](https://help.split.io/hc/en-us/articles/360020350372-PHP-SDK) & [GitHub](https://github.com/splitio/php-client) | +| PHP Thin-Client | server-side | [Docs](https://help.split.io/hc/en-us/articles/18305128673933-PHP-Thin-Client-SDK) & [GitHub](https://github.com/splitio/php-thin-client) | +| Python | server-side | [Docs](https://help.split.io/hc/en-us/articles/360020359652-Python-SDK) & [GitHub](https://github.com/splitio/python-client) | +| Ruby | server-side | [Docs](https://help.split.io/hc/en-us/articles/360020673251-Ruby-SDK) & [GitHub](https://github.com/splitio/ruby-client) | + +## Evaluator service + +For languages with no native SDK support, Split offers the Split Evaluator, a small service capable of evaluating all available features for a given customer via a REST endpoint. This service is available as a Docker container for ease of installation and is compatible with popular framework like Kubernetes when it comes to supporting standard health checks to achieve reliable uptimes. Learn more about the [Split evaluator](https://help.split.io/hc/en-us/articles/360020037072-Split-Evaluator). + +## Synchronizer service + +By default, Split's SDKs keep segment and feature flag definitions synchronized in an in-memory cache for speed at evaluating feature flags. However, some languages do not have a native capability to keep a shared local cache of this data to properly serve treatments. For these cases, we built Split Synchronizer to maintain an external cache like Redis. To learn more, read about [Split Synchronizer](https://help.split.io/hc/en-us/articles/360019686092-Split-Synchronizer). + +## Proxy service + +Split Proxy enables you to deploy a service in your own infrastructure that behaves like Split's servers and is used by both server-side and client-side SDKs to synchronize the flags without directly connecting to Split's backend. + +This tool reduces connection latencies between the SDKs and the Split server, and can be used when a single connection is required from a private network to the outside for security reasons. To learn more, read about [Split Proxy](https://help.split.io/hc/en-us/articles/4415960499213-Split-Proxy). + +## Supported agents + +Split's real user monitoring (RUM) agents collect detailed information about your users' experience when they visit your application. This information is used to analyze site impact, measure the degradation of performance metrics in relation to feature flag changes and alert the owner of the feature flag about such degradation. + +| **Agent** | **API Key/Type** | **Docs** | +| --- | --- | --- | +| Android | client-side | [Docs](https://help.split.io/hc/en-us/articles/18530305949837-Android-RUM-Agent) | +| iOS | client-side | [Docs](https://help.split.io/hc/en-us/articles/22545155055373-iOS-RUM-Agent) | +| Browser | client-side | [Docs](https://help.split.io/hc/en-us/articles/360030898431-Browser-RUM-Agent) | \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/_category_.json b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/_category_.json new file mode 100644 index 00000000000..533c8659e25 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Server-side SDK examples", + "collapsible": "true", + "collapsed": "true", + "className": "red", + "position": 7 +} \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/_category_.json b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/_category_.json new file mode 100644 index 00000000000..5a9ba25b317 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Server-side SDKs", + "collapsible": "true", + "collapsed": "true", + "className": "red", + "position": 6 +} \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/elixir-thin-client-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/elixir-thin-client-sdk.md new file mode 100644 index 00000000000..ae98f2f53e7 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/elixir-thin-client-sdk.md @@ -0,0 +1,321 @@ +--- +title: Elixir Thin Client SDK +sidebar_label: Elixir Thin Client SDK +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +This guide provides detailed information about our Elixir Thin SDK. All of our SDKs are open source. Go to our [Elixir Thin SDK GitHub repository](https://github.com/splitio/elixir-thin-client) to learn more. + +## Language support + +The Elixir Thin SDK supports Elixir language version v1.14.0 and later. + +## Architecture + +The Elixir Thin SDK depends on the [Split Daemon (splitd)](https://help.split.io/hc/en-us/articles/18305269686157) which should be set up on the same host. The Elixir Thin SDK client uses splitd to maintain the local cached copy of the Split rollout plan and return feature flag evaluations. + +## Initialization + +### 1. Install the SDK into your project + +The package can be installed by adding `split_thin_sdk` to your list of dependencies in `mix.exs`: + + + +```elixir +def deps do + [ + {:split, "~> 1.0.0", hex: :split_thin_sdk} + ] +end +``` + + + +The public release of the Elixir Thin SDK is available at [hex.pm](https://hex.pm/packages/split_thin_sdk), and API Reference is available at [hexdocs.pm](https://hexdocs.pm/split_thin_sdk/). + +### 2. Set up the splitd service + +Follow the guidance of our [Split Daemon (splitd)](https://help.split.io/hc/en-us/articles/18305269686157) doc to integrate splitd into your application infrastructure. + +:::warning[Supported link type] +The Elixir Thin SDK requires the Splitd daemon to be running with link type `unix-stream`. Update the `splitd.yaml` configuration file to include the following: + +```yaml +link: + type: unix-stream +``` +::: + +### 3. Start the SDK + +To start the Elixir Thin SDK, you need to start its supervisor, either in your Application's supervision tree, as a supervised child, or start it manually as an independent supervisor. + + + +```elixir +defmodule MyApp.Application do + use Application + + def start(_type, _args) do + children = [ + {Split, address: "/var/run/split.sock"} + ## other supervised children ... + ] + + opts = [strategy: :one_for_one, name: MyApp.Supervisor] + Supervisor.start_link(children, opts) + end +end +``` + + +```elixir +defmodule MyApp.Application do + use Application + + def start(_type, _args) do + Split.Supervisor.start_link(address: "/var/run/splitd.sock") + + children = [ + ## supervised children ... + ] + + opts = [strategy: :one_for_one, name: MyApp.Supervisor] + Supervisor.start_link(children, opts) + end +end +``` + + + +## Using the SDK + +### Basic use + +After you start the SDK, you can use the `Split.get_treatment/3` function to decide what version of your features your customers are served. The function requires the `FEATURE_FLAG_NAME` argument that you want to ask for a treatment and a unique `key` argument that corresponds to the end user that you want to serve the feature to. + +From there, you simply need to use an if-else-if or case statement block as shown below and insert the code for the different treatments that you defined in the Split user interface. Remember the final else branch in your code to handle the client returning [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). + +```elixir title="Elixir" +## The key here represents the string ID of the user/account/etc you're trying to evaluate a treatment for +treatment = Split.get_treatment(key, feature_flag_name) +case treatment do + "on" -> + ## Feature flag is enabled for this user + "off" -> + ## Feature flag is disabled for this user + _ -> + ## "control" treatment. For example, when feature flag is not found or Elixir SDK wasn't able to connect to Splitd +end +``` + +### Attribute syntax + +To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK's `get_treatment` function needs to pass an attribute map at runtime. + +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `get_treatment` call in a map. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in the Split Web Console to decide whether to show the `on` or `off` treatment to this account. + +The `get_treatment` function supports five types of attributes: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: + +* **Strings:** Use type String. +* **Numbers:** Use type Integer. +* **Dates:** Express the value in `seconds since epoch`. Use a timestamp represented by an Integer. +* **Booleans:** Use type Boolean. +* **Sets:** Use type List. + +```elixir title="Elixir" +attributes = %{ + "plan_type" => "growth", + "registered_date" => DateTime.to_unix(~U[2019-10-31 19:59:03Z], :second), + "deal_size" => 10000, + "paying_customer" => true, + "permissions" => ["gold", "silver", "platinum"] +}; + +treatment = Split.get_treatment("key", "FEATURE_FLAG_NAME", attributes); +``` + +### Multiple evaluations at once + +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `get_treatments` from the Split client to do this. +* `get_treatments`: Pass a list of the feature flag names you want treatments for. +* `get_treatments_by_flag_set`: Evaluate all flags that are part of the provided set name and are cached on the SDK instance. +* `get_treatments_by_flag_sets`: Evaluate all flags that are part of the provided set names and are cached on the SDK instance. + + + +```elixir +treatments = Split.get_treatments("key", ["FEATURE_FLAG_NAME_1", "FEATURE_FLAG_NAME_2"], nil); + +IO.inspect(treatments); +``` + + +```elixir +treatments = Split.get_treatments_by_flag_set("key", "backend", nil); + +IO.inspect(treatments); +``` + + +```elixir +treatments = Split.get_treatments_by_flag_sets("key", ["backend", "server_side"], nil); + +IO.inspect(treatments); +``` + + + +You can also use the [Split Manager](#manager) to get all of your treatments at once. + +### Get Treatments with Configurations + +To [leverage dynamic configurations with your treatments](https://help.split.io/hc/en-us/articles/360026943552), you should use the `Split.get_treatment_with_config/3` function. This function returns an `Split.TreatmentWithConfig` struct containing the treatment and associated configuration. + +The config element is a stringified version of the configuration JSON defined in the Split user interface. If there is no configuration defined for a treatment, the SDK returns `nil` for the config parameter. + +This function takes the exact same set of arguments as the standard `Split.get_treatment/3` function. See below for examples on proper usage: + +```elixir title="Elixir" +treatment_with_config = Split.get_treatment_with_config("KEY", "FEATURE_FLAG_NAME", attributes); + +%Split.TreatmentWithConfig{ treatment: treatment_value, config: config_json } = treatment_with_config; + +{:ok, config_map } = Jason.decode(config_json); +``` + +If you need to get multiple evaluations at once, you can also use the `Split.get_treatments_with_config/3` function. This function takes the exact same arguments as the [`get_treatments`](#multiple-evaluations-at-once) functions but return a map of feature flag names to `Split.TreatmentWithConfig` structs instead of strings. See example usage below: + +```elixir title="Elixir" +treatments_with_config = Split.get_treatments_with_config("KEY", ["FEATURE_FLAG_NAME_1", "FEATURE_FLAG_NAME_2"], attributes); +## treatments_with_config will have the following form: +## %{ +## "FEATURE_FLAG_NAME_1" => %Split.TreatmentWithConfig{treatment: "on", config: ~s({ "color": "red"})}, +## "FEATURE_FLAG_NAME_2" => %Split.TreatmentWithConfig{treatment: "v2", config: ~s({ "copy" : "better copy"})}, +## } +``` + +### Shutdown + +Due to the nature of the Elixir SDK, which uses the Split Daemon, there is no need to invoke any shutdown tasks. The data is stored and synchronized by the Split Daemon. + +## Track + +Use the `Split.track/5` function to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users’ actions and metrics. + +Refer to the [Events](https://help.split.io/hc/en-us/articles/360020585772) documentation for more information about using track events in feature flags. + +In the examples below you can see that the `Split.track/5` function can take up to five arguments. The proper data type and syntax for each are: + +* **key:** The `key` variable used in the `get_treatment` call and firing this track event. The expected data type is **String**. +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: + * Contains 63 characters or fewer. + * Starts with a letter or number. + * Contains only letters, numbers, hyphen, underscore, or period. + * This is the regular expression we use to validate the value:
`[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` +* **VALUE:** (Optional) The value to be used in creating the metric. This field can be sent in as `nil` or `0` if you intend to purely use the count function when creating a metric. The expected data type is **Integer** or **Float**. +* **PROPERTIES:** (Optional) A Map of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. + +The `track` function returns a boolean value of `true` or `false` to indicate whether or not the event was successfully queued to be sent back to Split's servers on the next event post. The SDK will return `false` if it wasn't able to connect to the Split Daemon, or if the current queue on the Split Daemon is full, or if an incorrect input to the `track` function has been provided. + +In the case that a bad input has been provided, you can read more about our SDK's expected behavior in the [Events documentation](https://help.split.io/hc/en-us/articles/360020585772-Track-events) + +```elixir title="Elixir" +// If you would like to send an event without a value +tracked? = Split.track("KEY", "TRAFFIC_TYPE", "EVENT_TYPE"); +// Example +tracked? = Split.track("john@doe.com", "user", "page_load_time"); + +// If you would like to associate a value to an event +tracked? = Split.track("KEY", "TRAFFIC_TYPE", "EVENT_TYPE", VALUE); +// Example +tracked? = Split.track("john@doe.com", "user", "page_load_time", 83.334); + +// If you would like to associate just properties to an event +tracked? = Split.track("KEY", "TRAFFIC_TYPE", "EVENT_TYPE", nil, PROPERTIES); + +// If you would like to associate a value and properties to an event +tracked? = Split.track("KEY", "TRAFFIC_TYPE", "EVENT_TYPE", VALUE, PROPERTIES); +// Example +properties = %{ + "package" => "premium", + "admin" => true, + "discount" => 50 +} +tracked? = Split.track("KEY", "TRAFFIC_TYPE", "EVENT_TYPE", 83.334, properties); +``` + +## Configuration + +The SDK takes a number of keyword arguments as options when the supervisor is started. The following options are available: + +- `:address`: **OPTIONAL** The path to the Splitd socket file. Default is `"/var/run/splitd.sock"`. +- `:pool_size`: **OPTIONAL** The size of the pool of connections to the Splitd daemon. Default is the number of online schedulers in the [Erlang VM](https://www.erlang.org/doc/apps/erts/erl_cmd.html). +- `:connect_timeout`: **OPTIONAL** The timeout in milliseconds to connect to the splitd daemon. Default is `1000`. + +```elixir title="Elixir" +opts = [ + address: "/var/run/splitd.sock", + pool_size: 1, + connect_timeout: 1000 +]; + +Split.Supervisor.start_link(opts); +``` + +## Manager + +Use the SDK Manager functions to get a list of available feature flags. + +You can access these functions through the `Split` module, as explained below: + +```elixir title="Manager functions" +## Retrieves the names of feature flags that are currently registered with the SDK +list_of_feature_flag_names = Split.split_names(); +## Example +## list_of_feature_flag_names = ["FEATURE_FLAG_NAME_1", "FEATURE_FLAG_NAME_2"]; + +## Returns the feature flag view for a given feature flag name +feature_flag_view = Split.split("FEATURE_FLAG_NAME"); +## Example +## feature_flag_view = %Split.SplitView{ name: "FEATURE_FLAG_NAME", ... }; + +## Retrieves the views of feature flags that are currently registered with the SDK. +list_of_feature_flag_views = Split.splits(); +## Example +## list_of_feature_flag_views = [%Split.SplitView{ name: "FEATURE_FLAG_NAME_1", ... }, %Split.SplitView{ name: "FEATURE_FLAG_NAME_2", ... }]; +``` + +The `Split.SplitView` struct referenced above has the following structure: + +```elixir title="SplitView struct" +@type t :: %Split.SplitView{ + name: String.t(), + traffic_type: String.t(), + killed: boolean(), + treatments: [String.t()], + change_number: integer(), + configs: %{String.t() => String.t() | nil}, + default_treatment: String.t(), + sets: [String.t()], + impressions_disabled: boolean() +} +``` + + + + + +## Example app + +* [Elixir app example](https://github.com/splitio/example-elixir) diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/go-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/go-sdk.md new file mode 100644 index 00000000000..faddfa04dd1 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/go-sdk.md @@ -0,0 +1,914 @@ +--- +title: Go SDK +sidebar_label: Go SDK +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +This guide provides detailed information about our Go SDK. All of our SDKs are open source. Go to our [Go SDK GitHub repository](https://github.com/splitio/go-client) to learn more. + +## Language support + +The Go SDK supports Go language version 1.18 and above. + +## Initialization + +### SDK architecture + +The Go SDK can run in three different modes to fit in different infrastructure configurations. + +* **in-memory-standalone:** The default (if no mode is specified) and most straightforward operation mode uses an in-memory storage to keep feature flags, segments, and queued impressions/metrics, as well as its own synchronization tasks that periodically keep feature flags and segments up to date, while flushing impressions and metrics to the Split backend. +* **redis-consumer:** This mode uses Redis as a broker to retrieve feature flags and segments and store impressions and metrics. It also requires the [Split Synchronizer](https://help.split.io/hc/en-us/articles/360019686092) to be running in the background, populating Redis with segments and feature flags, and flushing impressions and metrics to the Split backend. This mode is useful if you have multiple instances of Split's SDKs running (either in the same or a different language) and want to have a single synchronization point in your infrastructure. +* **localhost:** This mode should be used to stub the Split service when running local tests or development processes. It parses a file (either one specified by the user or `$HOME/.splits`) that defines feature flags and treatments to provide the developer with a predictable result of running `Treatment()` calls. + +### 1. Installing the SDK into your Go environment + +Since version 6, the Go SDK uses modules to handle all dependencies including itself, and due to semantic import versioning, both `dep` and bare-bones `go-get` are deprecated. To start using our SDK with modules, update your `go.mod` file as follows: + +```go title="go.mod" +require "github.com/splitio/go-client/v6 v6.7.0" +``` + +And update the import paths in your application to use the `v6` package suffix as follows: + +```go title="example.go" +import "github.com/splitio/go-client/v6/splitio/client +``` + +```go title="Go get" +go get github.com/splitio/go-client/v6@v6.7.0 +``` + +:::warning[If using Synchronizer with Redis - Synchronizer 2.x required after SDK Version 5.0.0] +Since version 2.0.0 of the split-synchronizer, we use a more efficient scheme to store impressions in Redis. This approach is faster and easier on your Redis instances, since it yields better throughput of impressions to the backend. If you use this SDK with the synchronizer in Redis or Proxy mode, you will need the newest versions of our Split synchronizer. It is recommended that once you're using SDK versions compatible with Split-Sync 2.0 on all your applications pointing to the Redis instance maintained by the Split-Sync, you disable backwards compatibility (this is as easy as changing a parameter to `true` on the JSON config or an environment variable to `on` if you're using the docker image). +::: + +### 2. Import the SDK into your project + +You can import the SDK into your project as shown below. + +```go title="go-client > v6.7.0" +import ( + "github.com/splitio/go-client/v6/splitio/client" + "github.com/splitio/go-client/v6/splitio/conf" +) +``` + +:::info[Using a wrapper] +Starting on version v6.0.0, every breaking change will require that you update your imports in cases where the SDK is used across multiple files. +It is recommended to create a wrapper that keeps it encapsulated. The package/file should be responsible for instantiating a single instance and exposing its functionality. +::: + +### 3. Instantiate the SDK and create a new Split client + +:::danger[If upgrading an existing SDK - Block until ready changes] +Starting version 4.0.0, cfg.BlockUntilReady is deprecated and migrated to the following implementation: +* Call `SplitClient#BlockUntilReady(int)` or `SplitManager#BlockUntilReady(int)`. +::: + +When the SDK is instantiated in `inmemory-standalone` operation mode, it kicks off background tasks to update an in-memory or Redis cache. + +This process can take up to a few hundred milliseconds depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while it is in this intermediate state, it may not have the data necessary to run the evaluation. In this circumstance, the SDK does not fail, but instead returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). + +To make sure the SDK is properly loaded before asking it for a treatment, you need to block until the SDK is ready. You can block by using the `BlockUntilReady(int)` method as part of the instantiation process of the SDK client as shown below. Do this as a part of the startup sequence of your application. + +Instantiating two (or more) different factories results in multiple instances of synchronization tasks, so you can have different instances of the SDK with different SDK Keys running within a single application. + +In the most common scenario, you should instantiate and reuse a single Split factory throughout your application. + +Configure the SDK with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split user interface, on your Admin settings page, API keys section. Select a server-side SDK API key. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. + +```go title="Go" +func main() { + cfg := conf.Default() + factory, err := client.NewSplitFactory("YOUR_SDK_KEY", cfg) + if err != nil { + fmt.Printf("SDK init error: %s\n", err) + return + } + + splitClient := factory.Client() + err = splitClient.BlockUntilReady(10) + if err != nil { + fmt.Printf("SDK timeout: %s\n", err) + return + } + // ... +} +``` + +Now you can start asking the SDK to evaluate treatments for your customers. + +## Using the SDK + +### Basic use + +After you instantiate the SDK client, you can start using the client's `Treatment` method to decide what version of your feature flags your customers are served. The method requires the `FEATURE_FLAG_NAME` attribute that you want to ask for a treatment and a unique `key` attribute that corresponds to the end user that you are serving the feature flag to. + +Then use an if-else-if block as shown below and insert the code for the different treatments that you defined in the Split user interface. Remember the final else branch in your code to handle the client returning the [control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). + +```go title="Go" +// The key here represents the ID of the user/account/etc you're trying to evaluate a treatment for +treatment := splitClient.Treatment("KEY", "FEATURE_FLAG_NAME", nil) +if treatment == "on" { + // insert code here to show on treatment +} else if treatment == "off" { + // insert code here to show off treatment +} else { + // insert your control treatment code here +} +``` + +The arguments for the `Treatment()` call are: + +* **key:** Either a string or a compound key that includes to strings. The matching key is used for evaluation purposes, and the bucketing key is used to determine the treatment based on the percentages specified in the feature flag creation user interface. +* **featureFlagName:** The name of the feature flag you are evaluating. +* **attributes:** A `map[string]interface{}` element that contains the attributes that are used by specific conditions that rely on information other than the key for matching purposes. + +### Attribute syntax + +To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK's `Treatment` method needs to be passed an attribute map at runtime. + +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `Treatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in the Split user interface to decide whether to show the `on` or off` treatment to this account. + +The `Treatment()` method supports five types of attributes: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: + +* **Strings:** Use type String. +* **Numbers:** Use type `int64`. +* **Dates: ** Express the value in `seconds since epoch` in `int64`. +* **Booleans:** Use type `bool`. +* **Sets:** Pass as `string slice ([]string)`. + +```go title="Go" + attributes := make(map[string]interface{}) + attributes["plan_type"] = "growth"; + attributes["registered_date"] = time.Now().UTC().Unix() + attributes["deal_size"] = 10000; + attributes["paying_customer"] = true; + attributes["permissions"] = []string{"read","write"} +``` + +### Multiple evaluations at once + +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `Treatments` from the Split client to do this. +* `Treatments`: Pass a list of the feature flag names you want treatments for. +* `TreatmentsByFlagSet`: Evaluate all flags that are part of the provided set name and are cached on the SDK instance. +* `TreatmentsByFlagSets`: Evaluates all flags that are part of the provided set names and are cached on the SDK instance. + + + + +```go +splitClient := factory.Client() +treatments := splitClient.Treatments( + "KEY", + []string{"FEATURE_FLAG_NAME_1", "FEATURE_FLAG_NAME_2", "FEATURE_FLAG_NAME_3"}, + nil, +) + +for featureFlag, treatment := range treatments { + fmt.Printf("Treatment for feature flag %s is %s\n", featureFlag, treatment) +} +``` + + +```go +splitClient := factory.Client() +treatments := splitClient.TreatmentsByFlagSet("KEY", "backend", nil) +``` + + +```go +splitClient := factory.Client() +treatments := splitClient.TreatmentsByFlagSets("KEY", []string{"backend", "server_side"}, nil) +``` + + + +### Get treatments with configurations + +To [leverage dynamic configurations with your treatments](https://help.split.io/hc/en-us/articles/360026943552), you should use the `TreatmentWithConfig` method. + +This method will return an object containing the treatment and associated configuration. + +The config element will be a stringified version of the configuration JSON defined in the Split user interface. If there are no configs defined for a treatment, the SDK returns `nil` for the config parameter. + +This method takes the exact same set of arguments as the standard `Treatment` method. See below for examples on proper usage: + +```go title="Go" +result := splitClient.TreatmentWithConfig("KEY", "FEATURE_FLAG_NAME", attributes) +var configs MyConfiguration // User custom configuration structure +if result.Config != nil { + err = json.Unmarshal([]byte(*result.Config), &config) + if err != nil { + fmt.Println("Error:", err) + } +} +treatment := result.Treatment +``` + +If you need to get multiple evaluations at once, you can also use the `TreatmentsWithConfig` methods. These methods take the exact same arguments as the [Treatments](#multiple-evaluations-at-once) methods but return a mapping of feature flag names to SplitResult objects instead of strings. Example usage below. + + + +```go +TreatmentResults := splitClient.TreatmentsWithConfig("KEY", []string{"FEATURE_FLAG_NAME_1", "FEATURE_FLAG_NAME_2"}, attributes) +// TreatmentResults will have the following form: +// { +// FEATURE_FLAG_NAME_1: {treatment: 'on', +// config: "{ 'color' : 'red'}}", +// FEATURE_FLAG_NAME_2: {treatment: 'v2', +// config: "{ 'copy' : 'better copy'}}", +// } +``` + + +```go +TreatmentResults := splitClient.TreatmentsWithConfigByFlagSet("KEY", "backend", attributes) +``` + + +```go +TreatmentResults := splitClient.TreatmentsWithConfigByFlagSets("KEY", []string{"backend", "server_side"}, attributes) +``` + + + +### Shutdown + +Call the `.Destroy()` method before letting a process using the SDK exit, as this method gracefully shuts down the Split SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. Call the `splitClient.Destroy()` method when the `kill` signal is cached by your application. After `.Destroy()` is called, any subsequent invocations to the `splitClient.Treatment()` or `manager` methods results in `control` or empty list, respectively. + +The example below shows how to catch the stop signal and call the `Destroy()` method. + +```go title="Go" +func main() { + sigs := make(chan os.Signal, 2) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) + + cfg := conf.Default() + factory, err := client.NewSplitFactory("YOUR_SDK_KEY", cfg) + + if err != nil { + fmt.Printf("SDK init error: %s\n", err) + return + } + + splitClient := factory.Client() + err = splitClient.BlockUntilReady(10) + if err != nil { + fmt.Printf("SDK timeout: %s\n", err) + return + } + + client := factory.Client() + + go func() { + <-sigs + splitClient.Destroy() + os.Exit(0) + }() +} +``` + +:::warning[Important!] +A call to the `destroy()` method also destroys the factory object. When creating new client instance, first create a new factory instance. +::: + +## Track + +Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users’ actions and metrics. + +[Learn more](https://help.split.io/hc/en-us/articles/360020585772) about using track events in feature flags. + +In the examples below, you can see that the `Track()` method can take up to five arguments. The proper data type and syntax for each are: + +* **key:** The `key` variable used in the `Treatment` call and firing this track event. The expected data type is **String**. +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: + * Contains 63 characters or fewer. + * Starts with a letter or number. + * Contains only letters, numbers, hyphen, underscore, or period. + * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` +* **VALUE:** (Optional) The value to be used in creating the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is **Integer** or **Float**. +* **PROPERTIES:** (Optional) A Map of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. + +:::warning[Redis Support] +If you are using our SDK with Redis, you need Split synchronizer **2.3.0** version at least in order to support *properties* in the `track` method. +::: + +```go title="Go" +// If you would like to send an event without a value +err = splitClient.Track("key", "TRAFFIC_TYPE", "EVENT_TYPE"); +// Example +err = splitClient.Track("john@doe.com", "user", "page_load_time"); + +// If you would like to associate a value to an event +err = splitClient.Track("key", "TRAFFIC_TYPE", "EVENT_TYPE", VALUE); +// Example +err= splitClient.Track("john@doe.com", "user", "page_load_time", 83.334) + +// If you would like to associate just properties to an event +err = splitClient.Track("key", "TRAFFIC_TYPE", "EVENT_TYPE", nil, {PROPERTIES}) + +// If you would like to associate a value and properties to an event +err = splitClient.Track("key", "TRAFFIC_TYPE", "EVENT_TYPE", VALUE, {PROPERTIES}) +// Example +properties := make(map[string]interface{}) +properties["package"] = "premium" +properties["admin"] = true +properties["discount"] = 50 + +err = splitClient.Track("key", "TRAFFIC_TYPE", "EVENT_TYPE", 83.334, properties) +``` + +## Configuration + +### Basic configuration + +The SDK has a number of knobs for configuring performance. Each knob is tuned to a reasonable default. However, you can override the value while instantiating the SDK. The parameters available for configuration are shown below. + +| **Field name(s)** | **Description** | **Default value** | +| --- | --- | --- | +| IPAddress | String in the format xxx.xxx.xxx.xxx containing the IP address that you want to be logged when submitting impressions. | By default, the SDK tries to figure out your IP address automatically. If this does not work, it falls back to *unknown*. | +| InstanceName | Unique identifier for this instance. | By default, the IP address is taken and the dots are replaced with hyphens. | +| LabelsEnabled | Whether impressions should include information about the label of the condition that matched. | true | +| Logger | User custom logger that must implement `logging.LoggerInterface` as described in the `go-toolkit` repo. | nil | +| LoggerConfig | Struct `logging.LoggerOptions` that allows you to customize the SDK's own logger. | See LoggerOptions section. | +| OperationMode | Defines how the SDK synchronizes and stores its data. Four operation modes are currently supported:
* `inmemory-standalone`
* `redis-consumer`
* `localhost` | `inmemory-standalone` | +| Redis | Describes the Redis connection information (host, port, etc.) and allows you to specify a prefix to avoid conflicts with other SDKs. | See Redis section. | +| SplitFile | Filename to be used when operating in `localhost` mode. | `.splits` within the user's home folder | +| TaskPeriods | Embedded struct that allows the developer to choose how frequently each synchronization task is run. | See TaskPeriods section. | +| IPAddressesEnabled | Flag to disable IP addresses and host name from being sent to the Split backend. | true | +| ImpressionsMode | Defines how impressions are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Split; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, all impressions are queued and sent to Split; this is useful for validations. Use DEBUG mode when you want every impression to be logged in Split's user interface when trying to debug your SDK setup. This setting does not impact the impression listener which receives all generated impressions locally. | `optimized` | + +The SDK factory receives two arguments, the API key and a pointer to a configuration structure. + +In most of our examples, we pass `nil` to use the default values. If we want to customize the SDK parameters we need to pass in the appropriate struct. + +To set each of the parameters defined above, use the following syntax. Use this syntax as an example of how to pass in a custom config. + +Note that we are not instantiating the struct on our own, but rather calling a `conf.Default()` method, which returns a valid configuration that you can then tailor to your specific needs. + +```go title="Go" +import ( + "fmt" + "github.com/splitio/go-client/splitio/client" + "github.com/splitio/go-client/splitio/conf" + "github.com/splitio/go-toolkit/logging" +) + +func main() { + sdkConf := conf.Default() + sdkConf.LoggerConfig.LogLevel = logging.LevelInfo + factory, err := client.NewSplitFactory("YOUR_SDK_KEY", sdkConf) + + // ... +} +``` + +### Advanced configuration + +These options are available under the `Advanced` property of the main configuration structure. + +| **Field name(s)** | **Description** | **Default value** | +| --- | --- | --- | +| HTTPTimeout | Timeout for HTTP calls in seconds. | 30 | +| SegmentQueueSize | Number of segments that can be queued for update (set to something greater than the number of segments your org has) | 500 | +| SegmentWorkers | Number of background tasks for updating segments. Set in conjunction with `SegmentQueueSize` based on the number of segments you have defined. | 10 | +| ImpressionListener | Custom implementation of impression listener interface. | nil | +| EventsBulkSize | Number of events to send per post. | 1000 | +| EventsQueueSize | Max number of events in the queue. Only in memory mode. | 10000 | +| ImpressionsQueueSize | Max number of impressions in the queue. Only in memory mode. | 10000 | +| StreamingEnabled | Boolean flag to enable the streaming service as default synchronization mechanism. In the event of an issue with streaming, the SDK will fallback to the polling mechanism. If false, the SDK will poll for changes as usual without attempting to use streaming. | true | +| FlagSetsFilter | This setting allows the SDK to only synchronize the feature flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the SDK instance, bringing all the benefits from a reduced payload. | empty | + +```go title="Go" +import ( + "fmt" + "github.com/splitio/go-client/splitio/client" + "github.com/splitio/go-client/splitio/conf" + "github.com/splitio/go-toolkit/logging" +) + +func main() { + sdkConf := conf.Default() + sdkConf.LoggerConfig.LogLevel = logging.LevelInfo + cfg.Advanced.FlagSetsFilter = []string{"backend", "server_side"} + factory, err := client.NewSplitFactory("YOUR_SDK_KEY", sdkConf) + + // ... +} +``` + +### Logger configuration + +These options customize the logger used by the SDK by default `"github.com/splitio/go-toolkit/logging"`. + +| **Field name(s)** | **Description** | **Default value** | +| --- | --- | --- | +|  ErrorWriter
 WarningWriter
 InfoWriter
 DebugWriter
 VerboseWriter | Each log-level has its own writer (a struct implementing Go's default interface `io.Writer`), meaning each level can be forwarded to a output stream. | By default all levels are configured to use Go's `os.Stdout` to output messages. | +| LogLevel | Minimum log level that makes it to its writer. Use constants defined in github.com/splitio/go-toolkit/logging:
 `logging.LevelError`
 `logging.LevelWarning`
 `logging.LevelInfo`
 `logging.LevelDebug`
 `logging.LevelVerbose` | `logging.LevelError` | + +### Redis configuration + +`redis-consumer` operation mode depends on Redis to function as the name suggests. + +The SDK configuration has the embedded struct `.Redis` where you can set up all of the parameters related to the Redis connection. + +| **Field name(s)** | **Description** | **Default value** | +| --- | --- | --- | +| Host | Hostname where the Redis instance is. | localhost | +| Port | HTTP port to be used in the connection | 6379 | +| Database | Numeric database to be used | 0 | +| Password | Redis cluster password. Leave empty if no password is used. | "" | +| Prefix | Best practice is to use a prefix in case the Redis instance is shared by many SDKs. | "" | +| TLSConfig | TLS configuration structure (ref: https://golang.org/pkg/crypto/tls/#Config) | False + +```go title="Go" +func main() { + cfg := conf.Default() + cfg.Redis.Host = "localhost" + cfg.Redis.Prefix = "go" + cfg.OperationMode = "redis-consumer" + factory, err := client.NewSplitFactory("YOUR_SDK_KEY", cfg) + if err != nil { + fmt.Printf("SDK init error: %s\n", err) + return + } + + splitClient := factory.Client() + err = splitClient.BlockUntilReady(10) + if err != nil { + fmt.Printf("SDK timeout: %s\n", err) + return + } + // ... +} +``` + +### Task periods + +When running in `inmemory-standalone` the SDK uses synchronization tasks that run in the background to keep the feature flags up to date and post impressions to the backend. +This configuration structure can be used to change the execution period of each of those tasks. + +| **Field name(s)** | **Description** | **Default value** | +| --- | --- | --- | +| * SplitSync
* SegmentSync
* ImpressionSync
* GaugeSync
* CounterSync
* LatencySync
* EventsSync | All of these parameters change the time to wait between subsequent executions of each task. | 30 | + +## Localhost mode + +For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the Split SDK can be started in localhost mode (also known as off-the-grid mode). In this mode, the SDK neither polls nor updates Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the feature flags. To use the SDK in localhost mode, you must replace the API key with localhost value. + +With this mode, you can instantiate the SDKS using one of the following methods: + +* JSON: Full support, for advanced cases or replicating an environment by pulling rules from Split cloud (from version `v6.3.0`). +* YAML: Supports dynamic configs, individual targets, and default rules (from version `4.0.0`). +* .split: Legacy option, only treatment result. + +### JSON + +Since version `v6.3.0`, our SDK supports localhost mode by using the JSON format. This version allows the user to map feature flags and segment definitions in the same format as the APIs receive the data. + +This new mode needs extra configuration to be set + +| **Name** | **Description** | **Type** | +| --- | --- | --- | +| splitFile | Indicates the path of the split file location to read | string | +| segmentDirectory | Indicates the path where all the segment files are located | string | +| localhostRefreshEnabled | Flag to run synchronization refresh for feature flags and segments in localhost mode. | bool | + +#### splitFile + +The following splitFile is a JSON that represents a SplitChange: + + + +```go +type SplitChangesDTO struct { + Till int64 `json:"till"` + Since int64 `json:"since"` + Splits []SplitDTO `json:"splits"` +} +``` + + +```go +type SplitDTO struct { + ChangeNumber int64 `json:"changeNumber"` + TrafficTypeName string `json:"trafficTypeName"` + Name string `json:"name"` + TrafficAllocation int `json:"trafficAllocation"` + TrafficAllocationSeed int64 `json:"trafficAllocationSeed"` + Seed int64 `json:"seed"` + Status string `json:"status"` + Killed bool `json:"killed"` + DefaultTreatment string `json:"defaultTreatment"` + Algo int `json:"algo"` + Conditions []ConditionDTO `json:"conditions"` + Configurations map[string]string `json:"configurations"` +} +``` + + +```json +{ + "splits": [ + { + "trafficTypeName": "user", + "name": "feature_flag_1", + "trafficAllocation": 100, + "trafficAllocationSeed": -1364119282, + "seed": -605938843, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1660326991072, + "algo": 2, + "configurations": {}, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "segment_1" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + } + ], + "label": "in segment segment_1" + }, + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 50 + }, + { + "treatment": "off", + "size": 50 + } + ], + "label": "default rule" + } + ] + } + ], + "since": -1, + "till": 1660326991072 +} +``` + + + +#### segmentDirectory + +The provided segment directory must have the JSON files of the corresponding segment linked to previous feature flag definitions. According to the Split file sample above, `feature_flag_1` has `segment_1` linked. That means that the segmentDirectory needs to have `segment_1` definition. + + + +```go +type SegmentChangesDTO struct { + Name string `json:"name"` + Added []string `json:"added"` + Removed []string `json:"removed"` + Since int64 `json:"since"` + Till int64 `json:"till"` +} +``` + + +```json +{ + "name": "segment_1", + "added": [ + "example1", + "example2" + ], + "removed": [], + "since": -1, + "till": 1585948850110 +} +``` + + + +```go title="Init example" +sdkConf := conf.Default() +sdkConf.SplitFile = "./splitChange.json" +sdkConf.SegmentDirectory = "./segments" +factory, err := client.NewSplitFactory("localhost", sdkConf) +``` + +### YAML + +Since version `4.0.0`, our SDK supports a type of localhost feature flag definition file that uses the YAML format. This new format allows the user to map different keys to different treatments within a single feature flag and also add configurations to them. The format is a list of single-key maps (one per mapping split-keys-config) which is defined as follows: + +```yaml title="YAML" +# - feature_flag_name: +# treatment: "treatment_applied_to_this_entry" +# keys: "single_key_or_list" +# config: "{\"desc\" : \"this applies only to ON treatment\"}" + +- my_feature: + treatment: "on" + keys: "key" + config: "{\"desc\" : \"this applies only to ON treatment\"}" +- some_other_feature: + treatment: "off" +- my_feature: + treatment: "off" +- other_feature: + treatment: "off" + keys: ["key_1", "key_2"] + config: "{\"desc\" : \"this overrides multiple keys and returns off treatment for those keys\"}" +``` + +In the example above, we have four entries: + + * The first entry defines that for feature flag `my_feature`, the key `key` returns the treatment `on` and the `on` treatment is tied to the configuration `{"desc" : "this applies only to ON treatment"}`. + * The second entry defines that the feature flag `some_other_feature` always returns the `off` treatment and no configuration. + * The third entry defines that `my_feature` always returns `off` for all keys that don't match another entry (in this case, any key other than `key`). + * The fourth entry shows how an example overrides a treatment for a set of keys. + +Use the SplitConfigBuilder object to set the location of the Split localhost YAML file as shown in the example below: + +```go title="Init example" +sdkConf := conf.Default() +sdkConf.SplitFile = "./splits.yaml" +factory, err := client.NewSplitFactory("localhost", sdkConf) +``` + +### .SPLIT file + +```go title="Go" +sdkConf := conf.Default() +factory, err := client.NewSplitFactory("localhost", sdkConf) +``` + +In this mode, the SDK loads a mapping of feature flag name to treatment from a file at `$HOME/.split`. For a given flag, the treatment specified in the file is returned for every customer. + +`getTreatment` calls for a feature flag and only returns the one treatment that you defined in the file. You can then change the treatment as necessary for your testing in the file. Any feature flag that is not provided in the `featureFlag` map returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment) if the SDK is asked to evaluate them. + +The format of this file is two columns separated by a whitespace. The left column is the feature flag name and the right column is the treatment name. The following is a sample `.split` file: + +```bash title="Shell" +reporting_v2 on ## sdk.getTreatment(*, reporting_v2) will return 'on' + +double_writes_to_cassandra off + +new-navigation v3 +``` + +## Manager + +Use the Split Manager to get a list of feature flags available to the Split client. + +To instantiate a Manager in your code base, use the same factory that you used for your client. + +```go title="Go" +package main + +import ( + "fmt" + "github.com/splitio/go-client/splitio/client" +) + +func main() { + factory, err := client.NewSplitFactory("YOUR_SDK_KEY", nil) + if err != nil { + fmt.Printf("SDK init error: %s\n", err) + return + } + + manager := factory.Manager() + + // ... +} +``` + +The Manager then has the following methods available. + +```go title="Manager Interface" +// Returns the names of all the feature flags in storage +SplitNames() []string +// Returns a partial view of all feature flags in the storage +Splits() []SplitView +// Returns a partial view of a single feature flag given it's name +Split(featureFlagName string) *SplitView +``` + +The `SplitView` object that you see referenced above has the following structure. + +```go title="SplitView" +type SplitView struct { + Name string `json:"name"` + TrafficType string `json:"trafficType"` + Killed bool `json:"killed"` + Treatments []string `json:"treatments"` + ChangeNumber int64 `json:"changeNumber"` + Configs map[string]string `json:"configs"` + DefaultTreatment string `json:"defaultTreatment"` + Sets []string `json:"sets"` + ImpressionsDisabled bool `json:"impressionsDisabled"` +} +``` + +## Listener + +Split SDKs send impression data back to Split servers periodically when evaluating feature flags. To send this information to a location of your choice, define and attach an *impression listener*. Use the SDK's `impressionListener` parameter, where you can add an implementation of `ImpressionListener`. This implementation **must** define the `LogImpression` method. It receives data in the following schema. + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| Impression | Impression | Impression object that has the feature flag name, treatment result, label, etc. | +| Attributes | map[string]interface{} | A list of attributes passed by the client. | +| InstanceID | string | The IP address of the machine running the SDK. | +| SDKLanguageVersion | string | The version of the SDK. In this case the language is `go` plus the version. | + +## Implement a custom impression listener + +Here is an example of how implement a custom impression listener. + +```go title="Custom listener example" +// Import ImpressionListener interface +import ( + "github.com/splitio/go-client/splitio/impressionListener" +) + +// Implementation Sample for a Custom Impression Listener +type CustomImpressionListener struct { +} + +func (i *CustomImpressionListener) LogImpression(data impressionlistener.ILObject) { + // Custom behavior +} +``` + +## Attach a custom impression listener + +Here is an example of how to implement a custom impression listener. + +```go title="Attach a listener" +import ( + "github.com/splitio/go-client/splitio/client" +) + +func main() { + customImpressionListener := &CustomImpressionListener{} + sdkConf := conf.Default() + sdkconf.Advanced.ImpressionListener = customImpressionListener + factory, err := client.NewSplitFactory("YOUR_SDK_KEY", sdkConf) + + // ... +} +``` + +## Logging + +SDK is intended to be embedded in other applications, so our logging should be the least intrusive possible. You can either supply your own logger (as long as it is wrapped in an adapter implementing the logger interface defined in our go-toolkit repo/logging package) or can use the SDK's own logger configuring its minimum `loglevel` and a writer for each of the levels. + +:::info[Note] +By default, only error-level messages are logged, and all writer outputs are set to `os.Stdout`. +::: + +### Custom logging + +To use a custom logger, implement the interface `LoggingInterface` from the `logging` package located in the `go-toolkit` repo, which is defined as follows. +```go title="Go" +type LoggerInterface interface { + Error(msg ...interface{}) + Warning(msg ...interface{}) + Info(msg ...interface{}) + Debug(msg ...interface{}) + Verbose(msg ...interface{}) +} +``` + +Below is an example of an HTTP logger sending messages to a server. + +:::info[Note] +It is the developer's responsibility to ensure that this logger's methods do not panic, and to handle logging levels if a custom logger is user. +::: + +```go title="Go" +type HttpLogger struct { + url string + level int +} + +// Generic function to be used by different log levels to send messages +func (l *HttpLogger) log(messages []string) { + // blocking call to send messages via http +} + +func (l *CustomLogger) Debug(msgs ...interface{}) { + if l.level < constants.DebugLevel { // constants defined by user + return + } + var messages []string + for _, msg := range msgs { + str, conv := msg.(string) + if conv { + messages = append(messages, str) + } + } + go log(messages) // async call to avoid blocking the SDK +} + +// Should implement Error, Warning, Info, Debug, Verbose. +// All with the same signature. + +func main() { + sdkConf := conf.Default() + sdkConf.Logger = &HttpLogger{url: "http://mylogs.io/"} + factory, err := client.NewSplitFactory("YOUR_SDK_KEY", sdkConf) + + // ... +} +``` + +### SDK logging + +In this example, we use our logging library's FileRotateWriter for the **Error** log level, which outputs logging messages to a file pattern until a max file size is reached, then switching to a new a file. + +```go title="Go" + var errorWriter io.Writer + errorWriter, err := logging.NewFileRotate(&logging.FileRotateOptions{ + BackupCount: 2, + MaxBytes: 1000000, + Path: os.Getenv("HOME") + "/splitErrors.log", + }) + + if err != nil { + fmt.Println("Error while instantiating writer, will fallback to stderr") + fmt.Println(err) + errorWriter = os.Stderr + } + + sdkConf := conf.Default() + sdkConf.LoggerConfig.LogLevel = logging.LevelInfo + sdkConf.LoggerConfg.ErrorWriter = errorWriter, + factory, err := client.NewSplitFactory("YOUR_SDK_KEY", sdkConfg) + // ... +``` + +## Proxy + +If you need to use a proxy, you can configure proxies by setting the environment variables `HTTP_PROXY` and `HTTPS_PROXY`. The SDK reads those variables and uses them to perform the server request. + +```go title="Example: Environment variables" +$ export HTTP_PROXY="http://10.10.1.10:3128" +$ export HTTPS_PROXY="http://10.10.1.10:1080" +``` diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/java-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/java-sdk.md new file mode 100644 index 00000000000..cf8cf12d46a --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/java-sdk.md @@ -0,0 +1,1535 @@ +--- +title: Java SDK +sidebar_label: Java SDK +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +This guide provides detailed information about our Java SDK. All of our SDKs are open source. Go to our [Java SDK GitHub repository](https://github.com/splitio/java-client) to see the source code. + +## Language support + +The Java SDK supports JDK8 and later. + +## Initialization + +To get started, set up Split in your code base using the following two steps. + +### 1. Import the SDK into your project + +Import the SDK into your project using one of the following two methods: + + + +```java + + io.split.client + java-client + 4.14.0 + +``` + + +```java +compile 'io.split.client:java-client:4.14.0' +``` + + + +If you cannot find the dependency, it may be due to the lag in the sync time between Sonatype and Maven central. In this case, use the following repository: + + + +```java + + + sonatype releases + https://oss.sonatype.org/content/repositories/releases/ + + +``` + + + +### 2. Instantiate the SDK and create a new Split client + +:::danger[If upgrading an existing SDK - Block until ready changes] +Starting version 3.0.1, SplitClientConfig#ready(int) is deprecated and migrated to a two part implementation: +* Set the desired value in `SplitClientConfig#setBlockUntilReadyTimeout(int)`. +* Call `SplitClient#blockUntilReady()` or `SplitManager#blockUntilReady()`. +::: + +When the SDK is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds, depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while it's in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). + +To make sure the SDK is properly loaded before asking it for a treatment, block until the SDK is ready. Do this by setting the desired wait using `.setBlockUntilReadyTimeout()` in the configuration and calling `blockUntilReady()` on the client. Do this all as a part of the startup sequence of your application. + +We recommend instantiating the Split factory once as a singleton and reusing it throughout your application. + +Use the code snippet below with your own API key. Configure the SDK with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split user interface, on your Admin settings page, API keys section. Select a server-side SDK API key. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. + + + +```java +import io.split.client.SplitClient; +import io.split.client.SplitClientConfig; +import io.split.client.SplitFactory; +import io.split.client.SplitFactoryBuilder; + +SplitClientConfig config = SplitClientConfig.builder() + .setBlockUntilReadyTimeout(10000) + .build(); + +SplitFactory splitFactory = SplitFactoryBuilder.build("YOUR_SDK_KEY", config); +SplitClient client = splitFactory.client(); +try { + client.blockUntilReady(); +} catch (TimeoutException | InterruptedException e) { + // log & handle +} +``` + + +```kotlin +import io.split.client.SplitClient +import io.split.client.SplitClientConfig +import io.split.client.SplitFactory +import io.split.client.SplitFactoryBuilder + +val config: SplitClientConfig = SplitClientConfig.builder() + .setBlockUntilReadyTimeout(10000) + .build() +val splitFactory: SplitFactory = SplitFactoryBuilder.build("YOUR_SDK_KEY", config) +val client: SplitClient = splitFactory.client() +try { + client.blockUntilReady() +} catch (e: Exception) { + // log & handle +} +``` + + + +Now you can start asking the SDK to evaluate treatments for your customers. + +## Using the SDK + +### Basic use + +After you instantiate the SDK client, you can start using the `getTreatment` method of the SDK client to decide what version of your features your customers are served. The method requires the `FEATURE_FLAG_NAME` attribute that you want to ask for a treatment and a unique `key` attribute that corresponds to the end user that you are serving the feature to. + +Then use an if-else-if block as shown below and insert the code for the different treatments that you defined in the Split user interface. Remember the final else branch in your code to handle the client returning the [control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). + + + +```java +// The key here represents the ID of the user/account/etc you're trying to evaluate a treatment for +String treatment = client.getTreatment("key","FEATURE_FLAG_NAME"); + +if (treatment.equals("on")) { + // insert code here to show on treatment +} else if (treatment.equals("off")) { + // insert code here to show off treatment +} else { + // insert your control treatment code here +} +``` + + +```kotlin +// The key here represents the ID of the user/account/etc you're trying to evaluate a treatment for +val treatment = client.getTreatment("key","FEATURE_FLAG_NAME") + +when (treatment) { + "on" -> { + // insert code here to show on treatment + } + "off" -> { + // insert code here to show off treatment + } + else -> { + // insert your control treatment code here + } +} +``` + + + +### Attribute syntax + +To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK's `getTreatment` method needs to be passed an attribute map at runtime. + +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the Rollout plan as defined in the Split user interface to decide whether to show the `on` or `off` treatment to this account. + +The `getTreatment` method supports five types of attributes: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: + +* **Strings:** Use type String. +* **Numbers:** Use type `java.lang.Long` or `java.lang.Integer`. +* **Dates:** Express the value in `milliseconds since epoch`. In Java, `milliseconds since epoch` is of type `java.lang.Long`. For example, the value for the `registered_date` attribute below is `System.currentTimeInMillis()`, which is a long. +* **Booleans:** Use type `java.lang.boolean`. +* **Sets:** Use type `java.util.Collection`. + + + +```java +import io.codigo.client.SplitClient; +import io.split.client.SplitFactory; +import io.split.client.SplitFactoryBuilder; + +import java.util.Map; +import java.util.HashMap; +import java.util.Date; + +SplitFactory splitFactory = SplitFactoryBuilder.build("YOUR_SDK_KEY"); +SplitClient client = splitFactory.client(); + +Map attributes = new HashMap(); +attributes.put("plan_type", "growth"); +attributes.put("registered_date", System.currentTimeMillis()); +attributes.put("deal_size", 1000); +attributes.put("paying_customer", true); +List perms = Arrays.asList("read", "write"); +attributes.put("permissions", perms); + +String treatment = client.getTreatment("key", "FEATURE_FLAG_NAME", attributes); + +if (treatment.equals("on")) { + // insert on code here +} else if (treatment.equals("off")) { + // insert off code here +} else { + // insert control code here +} +``` + + +```kotlin +import io.split.client.SplitClient +import io.split.client.SplitFactory +import io.split.client.SplitFactoryBuilder + +val splitFactory: SplitFactory = SplitFactoryBuilder.build("YOUR_SDK_KEY") +val client: SplitClient = splitFactory.client() + +val perms = listOf("read", "write") +val attributes = mapOf("plan_type" to "growth", + "registered_date" to System.currentTimeMillis(), + "deal_size" to 1000, + "paying_customer" to true, + "permissions" to perms) + +val treatment = client.getTreatment("key", "FEATURE_FLAG_NAME", attributes) + +when (treatment) { + "on" -> { + // insert code here to show on treatment + } + "off" -> { + // insert code here to show off treatment + } + else -> { + // insert your control treatment code here + } +} +``` + + + +### Multiple evaluations at once + +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` from the Split client to do this. +* `getTreatments`: Pass a list of the feature flag names you want treatments for. +* `getTreatmentsByFlagSet`: Evaluate all flags that are part of the provided set name and are cached on the SDK instance. +* `getTreatmentsByFlagSets`: Evaluate all flags that are part of the provided set names and are cached on the SDK instance. + + + +```java +// getTreatments +List featureFlagNames = Arrays.asList("FEATURE_FLAG_NAME_1", "FEATURE_FLAG_NAME_2"); +Map treatments = client.getTreatments("KEY", featureFlagNames); + +// getTreatmentsByFlagSet +Map treatmentsBySet = client.getTreatmentsByFlagSet("KEY", "backend"); + +// getTreatmentsByFlagSets +List flagSetNames = Arrays.asList("backend", "server_side"); +Map treatmentsBySets = client.getTreatmentsByFlagSets("KEY", flagSetNames); +``` + + +```kotlin +// getTreatments +val featureFlagNames = listOf("FEATURE_FLAG_NAME_1", "FEATURE_FLAG_NAME_2") +val treatments = client.getTreatments("KEY", featureFlagNames) + +// getTreatmentsByFlagSet +val treatmentsBySet = client.getTreatmentsByFlagSet("KEY", "backend") + +// getTreatmentsByFlagSets +val flagSetNames = listOf("backend", "server_side") +val treatmentsBySets = client.getTreatmentsByFlagSets("KEY", flagSetNames) +``` + + + +### Get treatments with configurations + +To [leverage dynamic configurations with your treatments](https://help.split.io/hc/en-us/articles/360026943552), you should use the `getTreatmentWithConfig` method. This method returns an object containing the treatment and associated configuration. + +The config element is a stringified version of the configuration JSON defined in the Split user interface. If there is no configuration defined for a treatment, the SDK returns `null` for the config parameter. + +This method takes the exact same set of arguments as the standard `getTreatment` method. See below for examples on proper usage: + + + +```java +SplitResult result = client.getTreatmentWithConfig("KEY", "FEATURE_FLAG_NAME"); +String treatment = result.treatment(); +if (null != result.config()) { + MyConfiguration config = gson.fromJson(result.config(), MyConfiguration.class); +} +``` + + +```kotlin +val result: SplitResult = client.getTreatmentWithConfig("KEY", "FEATURE_FLAG_NAME") +val config: String = result.config() +val treatment: String = result.treatment() +``` + + + +If you need to get multiple evaluations at once, you can also use the `getTreatmentsWithConfig` methods. These methods take the exact same arguments as the [getTreatments](#multiple-evaluations-at-once) methods but return a mapping of feature flag names to SplitResult instead of strings. Example usage below: + + + +```java +// getTreatmentsWithConfig +List featureFlagNames = Arrays.asList("FEATURE_FLAG_NAME_1", "FEATURE_FLAG_NAME_2"); +Map treatments = client.getTreatmentsWithConfig("KEY", featureFlagNames, attributes); + +// getTreatmentsWithConfigByFlagSet +Map treatmentsBySet = client.getTreatmentsWithConfigByFlagSet("KEY", "backend"); + +// getTreatmentsWithConfigByFlagSets +List flagSetNames = Arrays.asList("backend", "server_side"); +Map treatmentsBySets = client.getTreatmentsWithConfigByFlagSets("KEY", flagSetNames); +``` + + +```kotlin +// getTreatmentsWithConfig +val featureFlagNames = listOf("FEATURE_FLAG_NAME_1", "FEATURE_FLAG_NAME_2") +val treatments: Map = client.getTreatmentsWithConfig("KEY", featureFlagNames) + +// getTreatmentsWithConfigByFlagSet +val treatmentsBySet: Map = client.getTreatmentsWithConfigByFlagSet("KEY", "backend") + +// getTreatmentsWithConfigByFlagSets +val flagSetNames = listOf("backend", "server_side") +val treatmentsBySets: Map = client.getTreatmentsWithConfigByFlagSets("KEY", flagSetNames) +``` + + + +### Shutdown + +Make sure to call `.destroy()` before letting a process using the SDK exit as it gracefully shuts down the Split SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions and events. The Java SDK specifically subscribes to the JVM shutdown hook (SIGTERM signal) which in normal circumstances is invoked automatically by the JVM during a shutdown process. This means that on a graceful shutdown of the server, the client will automatically call destroy() and will flush the buffers and release the resources. + +In cases where you don't want our SDK to automatically destroy on shutdown, you can use the config: `disableDestroyOnShutDown()` (example usage in the [Configuration](#configuration) section below) and set it to `true`. If you do this, the SDK ignores any signals like SIGTERM and it is your responsibility to properly call destroy at the right time. If a manual shutdown is required, you can then call: + + + +```java +client.destroy(); +``` + + +```kotlin +client.destroy() +``` + + + +After `destroy()` is called, any subsequent invocations to `client.getTreatment()` or manager methods result in `control` or empty list, respectively. + +:::warning[Important!] +A call to the `destroy()` method also destroys the factory object. When creating new client instance, first create a new factory instance. +::: + +## Track + +Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users' actions and metrics. + +Refer to the [Events](https://help.split.io/hc/en-us/articles/360020585772) guide for more information about using track events in feature flags. + +In the examples below you can see that the `.track()` method can take up to five arguments. The proper data type and syntax for each are: + +* **key:** The `key` variable used in the `getTreatment` call and firing this track event. The expected data type is **String**. +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you defined in your instance of Split. +* **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: + * Contains 63 characters or fewer. + * Starts with a letter or number. + * Contains only letters, numbers, hyphen, underscore, or period. + * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` +* **VALUE:** (Optional) The value used in creating the metric. This field can be sent in as null or 0 if you intend to only use the count function when creating a metric. The expected data type is **Integer** or **Float**. +* **PROPERTIES:** (Optional) A map of key value pairs that can filter your metrics. To learn more about event property capture, refer to the [Events property capture](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. + +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK successfully queued the event to be sent back to Split's servers on the next event post. The SDK returns `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method is provided. + +In the case that a bad input has been provided, you can read more about our SDK's expected behavior in our [Events](https://help.split.io/hc/en-us/articles/360020585772-Track-events) guide. + + + +```java +// If you would like to send an event without a value +boolean trackEvent = client.track("key", "TRAFFIC_TYPE", "EVENT_TYPE"); +// Example +boolean trackEvent = client.track("john@doe.com", "user", "page_load_time"); + +// If you would like to associate a value to an event +boolean trackEvent = client.track("key", "TRAFFIC_TYPE", "EVENT_TYPE", VALUE); +// Example +boolean trackEvent = client.track("john@doe.com", "user", "page_load_time", 83.334); + +// If you would like to associate a value and properties to an event +boolean trackEvent = client.track("key", "TRAFFIC_TYPE", "EVENT_TYPE", VALUE, {PROPERTIES}); +// Example +HashMap properties = new HashMap<>(); +properties.put("package", "premium"); +properties.put("admin", true); +properties.put("discount", 50); + +boolean trackEvent = client.track("john@doe.com", "user", "page_load_time", 83.334, properties); + +// If you would like to associate just properties to an event +boolean trackEvent = client.track("key", "TRAFFIC_TYPE", "EVENT_TYPE", {PROPERTIES}); +// Example +HashMap properties = new HashMap<>(); +properties.put("package", "premium"); +properties.put("admin", true); +properties.put("discount", 50); + +boolean trackEvent = client.track("john@doe.com", "user", "page_load_time", properties); +``` + + +```kotlin +// If you would like to send an event without a value +val trackEvent: Boolean = client.track("key", "TRAFFIC_TYPE", "EVENT_TYPE") +// Example +val trackEvent: Boolean = client.track("john@doe.com", "user", "page_load_time") + +// If you would like to associate a value to an event +val trackEvent: Boolean = client.track("key", "TRAFFIC_TYPE", "EVENT_TYPE", VALUE) +// Example +val trackEvent: Boolean = client.track("john@doe.com", "user", "page_load_time", 83.334) + +// If you would like to associate a value and properties to an event +val trackEvent: Boolean = client.track("key", "TRAFFIC_TYPE", "EVENT_TYPE", VALUE, {PROPERTIES}) +// Example +val properties = mapOf("package" to "premium", + "admin" to true, + "discount" to 50) + +val trackEvent: Boolean = client.track("john@doe.com", "user", "page_load_time", 83.334, properties) + +// If you would like to associate just properties to an event +val trackEvent: Boolean = client.track("key", "TRAFFIC_TYPE", "EVENT_TYPE", {PROPERTIES}) +// Example +val properties = mapOf("package" to "premium", + "admin" to true, + "discount" to 50) + +val trackEvent: Boolean = client.track("john@doe.com", "user", "page_load_time", properties) +``` + + + +## Configuration + +The SDK has a number of knobs for configuring performance. Each knob is tuned to a reasonable default. However, you can override the value while instantiating the SDK. The parameters available for configuration are shown below. + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| featuresRefreshRate | The SDK polls Split servers for changes to feature rollout plans. This parameter controls this polling period in seconds. | 60 seconds | +| segmentsRefreshRate | The SDK polls Split servers for changes to segments at this rate (in seconds). | 60 seconds | +| impressionsRefreshRate | The treatment log captures which customer saw what treatment (on, off, etc.) at what time. This log is periodically flushed back to Split servers. This configuration controls how quickly the cache expires after a write (in seconds). | 60 seconds | +| telemetryRefreshRate | The SDK caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers (in seconds). | 3600 seconds | +| eventsQueueSize | When using `.track`, the number of events to be kept in memory. | 500 | +| eventFlushIntervalInMillis | When using `.track`, how often (in milliseconds) the events queue is flushed to Split servers. | 30000 ms | +| connectionTimeout | HTTP client connection timeout (in ms). | 15000ms | +| readTimeout | HTTP socket read timeout (in ms). | 15000ms | +| setBlockUntilReadyTimeout | If specified, the client building process blocks until the SDK is ready to serve traffic or the specified time has elapsed. If the SDK is not ready within the specified time, a `TimeOutException` is thrown (in ms). | 0ms | +| impressionsQueueSize | Default queue size for impressions. | 30K | +| disableLabels | Disable labels from being sent to Split backend. Labels may contain sensitive information. | enabled | +| disableIPAddress | Disable sending IP Address & hostname to the backend. | enabled | +| proxyHost | The location of the proxy. | localhost | +| proxyPort | The port of the proxy. | -1 (not set) | +| proxyUsername | Username to authenticate against the proxy server. | null | +| proxyPassword | Password to authenticate against the proxy server. | null | +| streamingEnabled | Boolean flag to enable the streaming service as default synchronization mechanism. In the event of an issue with streaming, the SDK falls back to the polling mechanism. If false, the SDK polls for changes as usual without attempting to use streaming. | true | +| impressionsMode | Defines how impressions are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Split; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, all impressions are queued and sent to Split; this is useful for validations. Use DEBUG mode when you want every impression to be logged in the Split user interface when trying to debug your SDK setup. This setting does not impact the impression listener which receives all generated impressions locally. | OPTIMIZED | +| operationMode | Defines how the SDK synchronizes its data. Two operation modes are currently supported:
- STANDALONE.
- CONSUMER| STANDALONE | +| storageMode | Defines what kind of storage the SDK is going to use. With MEMORY, the SDK uses its own storage and runs as STANDALONE mode. Set REDIS mode if you want the SDK to run with this implementation as CONSUMER mode. | MEMORY | +| flagSetsFilter | This setting allows the SDK to only synchronize the feature flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the SDK instance, bringing all the benefits from a reduced payload. | null | +| threadFactory | Defines what kind of thread the SDK is going to use. Allows the SDK to use Virtual Threads. | null | +| inputStream | This setting allows the SDK supports InputStream to use localhost inside a JAR. | null | +| FileTypeEnum | Defines which kind of file is going to be the inputStream. Supported files are YAML and JSON for inputStream. | null | + +To set each of the parameters defined above, use the following syntax: + + + +```java +import io.split.client.SplitClient; +import io.split.client.SplitClientConfig; +import io.split.client.SplitFactory; +import io.split.client.SplitFactoryBuilder; + +SplitClientConfig config = SplitClientConfig.builder() + .impressionsRefreshRate(60) + .connectionTimeout(15000) + .readTimeout(15000) + .enableDebug() + .setBlockUntilReadyTimeout(10000) + .flagSetsFilter(Arrays.asList("backend", "server_side")) + .build(); + +SplitFactory splitFactory = SplitFactoryBuilder.build("YOUR_SDK_KEY",config); +SplitClient client = splitFactory.client(); +client.blockUntilReady(); +``` + + +```kotlin +import io.split.client.SplitClient; +import io.split.client.SplitClientConfig; +import io.split.client.SplitFactory; +import io.split.client.SplitFactoryBuilder; + +val config: SplitClientConfig = SplitClientConfig.builder() + .impressionsRefreshRate(60) + .connectionTimeout(15000) + .readTimeout(15000) + .enableDebug() + .setBlockUntilReadyTimeout(10000) + .flagSetsFilter(Arrays.asList("backend", "server_side")) + .build() + +val splitFactory: SplitFactory = SplitFactoryBuilder.build("YOUR_SDK_KEY", config) +val client: SplitClient = splitFactory.client() +client.blockUntilReady() +``` + + + +## Connecting to a Split Proxy instance + +The SDK can connect to a Split Proxy instance as though it was connecting to our CDN, and the Proxy synchronizes the data and writes impressions and events back to the Split server. Be sure to install the Split Proxy by following the steps in [Split Proxy guide](https://help.split.io/hc/en-us/articles/4415960499213-Split-Proxy). + +Use the `.endpoint()` property in the SplitClientConfig builder object to point the Java SDK to the Synchronizer, making sure to use the same port specified in the Proxy command line. When creating the `SplitFactory` object, use the custom API key specified in the `client-apikeys` parameter for the Proxy. The Proxy uses the Split SDK key when connecting to Split. Refer to the following code example to connect to a Proxy instance: + + + +```java +import io.split.client.SplitClient; +import io.split.client.SplitClientConfig; +import io.split.client.SplitFactory; +import io.split.client.SplitFactoryBuilder; + +public class SplitSD { + public static void main(String[] args) { + SplitClientConfig config = SplitClientConfig.builder() + .setBlockUntilReadyTimeout(10000) + .endpoint("https://myproxy.com","https://myproxy.com") + .authServiceURL("https://myproxy.com" + "/api/auth") + .telemetryURL("https://myproxy.com" + "/api/v1") + .build(); + SplitFactory splitFactory = SplitFactoryBuilder.build("YOUR_SDK_KEY", config); + SplitClient client = splitFactory.client(); + try { + client.blockUntilReady() + String treatment = client.getTreatment("user10","sample_feature_flag"); + if (treatment.equals("on")) { + System.out.print("Treatment is on"); + } else if (treatment.equals("off")) { + System.out.print("Treatment is off"); + } else { + System.out.print("SDK Not ready"); + } + } catch (Exception e) { + System.out.print("Exception: "+e.getMessage()); + } + } +} +``` + + +```kotlin +import io.split.client.SplitFactoryBuilder +import io.split.client.SplitClient +import io.split.client.SplitClientConfig +import io.split.client.SplitFactory + +fun main (args: Array){ + val config: SplitClientConfig = SplitClientConfig.builder() + .setBlockUntilReadyTimeout(10000) + .endpoint("https://myproxy.com","https://myproxy.com") + .authServiceURL("https://myproxy.com" + "/api/auth") + .telemetryURL("https://myproxy.com" + "/api/v1") + .build() + val splitFactory: SplitFactory = SplitFactoryBuilder.build("YOUR_SDK_KEY", config) + val client: SplitClient = splitFactory.client() + try { + client.blockUntilReady() + val treatment = client.getTreatment("key", "FEATURE_FLAG_NAME", attributes) + when (treatment) { + "on" -> { + println("Treatment is on") + } + "off" -> { + println("Treatment is off") + } + else -> { + println("SDK Not ready") + } + } + } catch (e: Exception) { + println("Exception: " + e.message) + } +} +``` + + + +## Localhost mode + +For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the Split SDK can be started in **localhost** mode (aka off-the-grid mode). In this mode, the SDK neither polls nor updates Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the features. To use the SDK in localhost mode, you must replace the API Key with "localhost" value. + +With this mode, you can instantiate the SDKS using one of the following methods: + +* JSON: Full support, for advanced cases or replicating an environment by pulling rules from Split cloud (from version `4.7.0`). +* YAML: Supports dynamic configs, individual targets and default rules (from version `3.1.0`). +* .split: Legacy option, only treatment result. + +### JSON + +Since version `4.7.0`, our SDK supports localhost mode by using the JSON format. This version allows the user to map feature flags and segment definitions in the same format as the APIs receive the data. + +This new mode needs extra configuration to be set + +| **Name** | **Description** | **Type** | +| --- | --- | --- | +| splitFile | Indicates the path of the feature flags file location to read | String | +| segmentDirectory | Indicates the path where all the segment files are located | String | +| localhostRefreshEnabled | Flag to run synchronization refresh for feature flags and segments in localhost mode. | Boolean | + +#### splitFile + +The following splitFile is a JSON that represents a SplitChange: + + + +```java +public class SplitChange { + public List splits; + public long since; + public long till; +} +``` + + +```java +public class Split { + public String name; + public int seed; + public Status status; + public boolean killed; + public String defaultTreatment; + public List conditions; + public String trafficTypeName; + public long changeNumber; + public Integer trafficAllocation; + public Integer trafficAllocationSeed; + public int algo; + public Map configurations; +} +``` + + +```kotlin +class Split( + var splits: List?, + var since: Long?, + var till: Long?, +) +``` + + +```kotlin +class Split( + var name: String?, + var seed: Int?, + var status: Status?, + var killed: Boolean?, + var defaultTreatment: String?, + var conditions: List?, + var trafficTypeName: String?, + var changeNumber: Long?, + var trafficAllocation: Int?, + var trafficAllocationSeed: Int?, + var algo: Int?, + configurations: Map? +) +``` + + +```json +{ + "splits": [ + { + "trafficTypeName": "user", + "name": "feature_flag_1", + "trafficAllocation": 100, + "trafficAllocationSeed": -1364119282, + "seed": -605938843, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1660326991072, + "algo": 2, + "configurations": {}, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "segment_1" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + } + ], + "label": "in segment segment_1" + }, + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 50 + }, + { + "treatment": "off", + "size": 50 + } + ], + "label": "default rule" + } + ] + } + ], + "since": -1, + "till": 1660326991072 +} +``` + + + +#### segmentDirectory + +The provided segment directory must have the json files of the corresponding segment linked to previous feature flag definitions. According to the Split file sample above: `feature_flag_1` has `segment_1` linked. That means that the segmentDirectory needs to have `segment_1` definition. + + + +```java +public class SegmentChange { + public String id; + public String name; + public List added; + public List removed; + public long since; + public long till; +} +``` + + +```kotlin +class SegmentChange( + var id: String, + var name: String, + var added: List, + var removed: List, + var since: Long, + var till: Long +) +``` + + +```json +{ + "name": "segment_1", + "added": [ + "example1", + "example2" + ], + "removed": [], + "since": -1, + "till": 1585948850110 +} +``` + + + + + +```java +SplitClientConfig config = SplitClientConfig.builder() + .splitFile("parentRoot/featureFlags.json") + .segmentDirectory("parentRoot/segments") + .setBlockUntilReadyTimeout(10000) + .build(); +``` + + +```kotlin +val config: SplitClientConfig = SplitClientConfig.builder() + .splitFile("parentRoot/featureFlags.json") + .segmentDirectory("parentRoot/segments") + .setBlockUntilReadyTimeout(10000) + .build() +``` + + + +### YAML + +Since version `3.1.0`, our SDK supports a type of localhost feature flag definition file that uses the YAML format. This new format allows the user to map different keys to different treatments within a single feature flag and also add configurations to them. The format is a list of single-key maps (one per mapping feature-flag-keys-config) which is defined as follows: + + + +```yaml +## - feature_name: +## treatment: "treatment_applied_to_this_entry" +## keys: "single_key_or_list" +## config: "{\"desc\" : \"this applies only to ON treatment\"}" + +- my_feature_flag: + treatment: "on" + keys: "key" + config: "{\"desc\" : \"this applies only to ON treatment\"}" +- some_other_feature_flag: + treatment: "off" +- my_feature_flag: + treatment: "off" +- other_feature_flag: + treatment: "off" + keys: ["key_1", "key_2"] + config: "{\"desc\" : \"this overrides multiple keys and returns off treatment for those keys\"}" +``` + + + +In the example above, we have four entries: + + * The first entry defines that for feature flag `my_feature_flag`, the key `key` returns the treatment `on` and the `on` treatment is tied to the configuration `{"desc" : "this applies only to ON treatment"}`. + * The second entry defines that the feature flag `some_other_feature_flag` always returns the `off` treatment and no configuration. + * The third entry defines that `my_feature_flag` always returns `off` for all keys that don't match another entry (in this case, any key other than `key`). + * The fourth entry shows how an example overrides a treatment for a set of keys. + +Use the SplitConfigBuilder object to set the location of the Split localhost YAML file as shown in the example below: + + + +```java +import io.split.client.SplitClient; +import io.split.client.SplitClientConfig; +import io.split.client.SplitFactoryBuilder; + +SplitClientConfig config = SplitClientConfig.builder() + .splitFile("parentRoot/split.yaml") + .setBlockUntilReadyTimeout(10000) + .build(); +SplitClient client = SplitFactoryBuilder.build("localhost", config).client(); +``` + + +```kotlin +import io.split.client.SplitFactoryBuilder +import io.split.client.SplitClient +import io.split.client.SplitClientConfig + +val config: SplitClientConfig = SplitClientConfig.builder() + .splitFile("parentRoot/split.yaml") + .setBlockUntilReadyTimeout(10000) + .build() +val client: SplitClient = SplitFactoryBuilder.build("localhost", config).client() +``` + + + +### .SPLIT file + + + +```java +SplitClientConfig config = SplitClientConfig.builder().setBlockUntilReadyTimeout(10000).build(); +SplitFactory splitFactory = SplitFactoryBuilder.build("localhost", config); +SplitClient client = splitFactory.client(); +``` + + +```kotlin +val config: SplitClientConfig = SplitClientConfig.builder().setBlockUntilReadyTimeout(10000).build() +val splitFactory: SplitFactory = SplitFactoryBuilder.build("localhost", config) +val client: SplitClient = splitFactory.client() +``` + + + +In this mode, the SDK loads a mapping of feature flag name to treatment from a file at `$HOME/.split`. For a given flag, the treatment specified in the file is returned for every customer. + +`getTreatment` calls for a feature flag and only returns the one treatment that you defined in the file. You can then change the treatment as necessary for your testing in the file. Any feature that is not provided in the `features` map returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment) if the SDK is asked to evaluate them. + +The format of this file is two columns separated by a whitespace. The left column is the feature flag name and the right column is the treatment name. The following is a sample `.split` file: + +```bash title="Shell" +reporting_v2 on ## sdk.getTreatment(*, reporting_v2) will return 'on' + +double_writes_to_cassandra off + +new-navigation v3 +``` + +### Input Stream + +Since version `4.9.0`, the SDK supports InputStream to use localhost inside a JAR. To achieve this, we added new parameters in splitFile property to set the InputStream. The first param is an InputStream of the file that we want to read. And the second is a FileTypeEnum which can be either `YAML`, or `JSON`. Here is an example code to demonstrate how to use this new feature: + + + +```java +import io.split.client.SplitClient; +import io.split.client.SplitClientConfig; +import io.split.client.SplitFactoryBuilder; +import io.split.client.utils.FileTypeEnum; + +import java.io.FileInputStream; +import java.io.InputStream; + +InputStream inputStream = new FileInputStream("parentRoot/split.yaml"); +SplitClientConfig config = SplitClientConfig.builder() + .splitFile(inputStream, FileTypeEnum.YAML) + .setBlockUntilReadyTimeout(10000) + .build(); +SplitClient client = SplitFactoryBuilder.build("localhost", config).client(); +``` + + +```java +import io.split.client.SplitFactoryBuilder; +import io.split.client.SplitClient; +import io.split.client.SplitClientConfig; +import io.split.client.utils.FileTypeEnum; + +import java.io.FileInputStream; +import java.io.InputStream; + +InputStream inputStream = new FileInputStream("parentRoot/split.json"); +SplitClientConfig config = SplitClientConfig.builder() + .splitFile(inputStream, FileTypeEnum.JSON) + .setBlockUntilReadyTimeout(10000) + .build(); +SplitClient client = SplitFactoryBuilder.build("localhost", config).client(); +``` + + +```kotlin +import io.split.client.SplitFactoryBuilder +import io.split.client.SplitClient +import io.split.client.SplitClientConfig +import io.split.client.utils.FileTypeEnum + +import java.io.FileInputStream +import java.io.InputStream + +val inputStream = new FileInputStream("parentRoot/split.json"); +val config: SplitClientConfig = SplitClientConfig.builder() + .splitFile(inputStream, FileTypeEnum.JSON) + .setBlockUntilReadyTimeout(10000) + .build() +val client: SplitClient = SplitFactoryBuilder.build("localhost", config).client() +``` + + + +## State Sharing: Redis Integration + +Before you get started with the cache, download the correct version of Redis to your machine. Make sure to start your Redis server. Refer to the [Redis documentation](https://redis.io/topics/quickstart) for help. After that, followi the additional three steps to set up the cache with Redis. + +#### 1. Install the Redis Wrapper into your project + +Import the Redis Wrapper into your project using one of the two methods below: + + + +```java + + io.split.client + redis-wrapper + 3.1.1 + +``` + + +```java +compile 'io.split.client:redis-wrapper:1.0.0' +``` + + + +#### 2. Set up the Split Synchronizer + + Set up the [Split Synchronizer](https://help.split.io/hc/en-us/articles/360019686092) to sync data to a Redis cache. Once you set up the synchronizer, go to the following step #3 to instantiate: + +#### 3. Instantiate the SDK client with Redis enabled + +To run the SDK with Redis, you need to provide the Redis storage wrapper. Refer to the following to provide the wrapper: + + + +```java +// Building the Redis storage wrapper with some configurations of choice. +CustomStorageWrapper redis = RedisInstance.builder() + .host("localhost") + .port(6379) + .timeout(1000) + .database(0) + .prefix("java") + .build(); +// Building the SDK config with the Redis wrapper referenced. +SplitClientConfig splitConfig = SplitClientConfig.builder() + .customStorageWrapper(redis) + .operationMode(OperationMode.CONSUMER) + .storageMode(StorageMode.REDIS) + .build(); +// Then just build the factory as usual. +SplitFactory splitFactory = SplitFactoryBuilder.build("YOUR_API_KEY", config); +``` + + +```kotlin +// Building the Redis storage wrapper with some configurations of choice. +val redis: CustomStorageWrapper = RedisInstance.builder() + .host("localhost") + .port(6379) + .timeout(1000) + .database(0) + .prefix("java") + .build() +// Building the SDK config with the Redis wrapper referenced. +val splitConfig: SplitClientConfig = SplitClientConfig.builder() + .customStorageWrapper(redis) + .operationMode(OperationMode.CONSUMER) + .storageMode(StorageMode.REDIS) + .build() +// Then just build the factory as usual. +val splitFactory: SplitFactory = SplitFactoryBuilder.build("YOUR_API_KEY", config) +``` + + + +#### Redis wrapper configuration + +When you create a new instance for the Redis wrapper, you can provide your own configurations for some values. + +| **Field name(s)** | **Description** | **Default value** | +| --- | --- | --- | +| timeout| Timeout that the connections is going to handle. | 1000 | +| host | Hostname where the Redis instance is. | localhost | +| port | HTTP port used in the connection. | 6379 | +| database | Numeric database to be used. | 0 | +| user | Redis cluster user. Leave empty if no User is used. | "" | +| password | Redis cluster password. Leave empty if no password is used. | "" | +| prefix | Best practice is to use a prefix in case the Redis instance is shared by many SDKs. | "" | +| jedisPool| You can provide your own implementation of JedisPool. | null | +| maxTotal| Max number of pool connections. | 8 | + +#### Redis cluster support + +The SDK supports Redis with Cluster. Note that a stable release of Cluster has shipped since Redis 3.0. For further information about Redis Cluster, refer to the [Cluster documentation](https://redis.io/topics/cluster-spec). + +Use the following configuration for Redis in Cluster mode. + +| **Variable** | **Type** | **Description** | +| --- | --- | --- | +| clusterNodes | Set\ | The list of cluster nodes. | +| jedis | JedisCluster | Jedis contains the list of cluster nodes. | +| keyHashTag | string | Custom hashtag to be used. | + + + +```java +// Building the Redis storage wrapper with some configurations of choice. +Set jedisClusterNodes = new HashSet(); +jedisClusterNodes.add(new HostAndPort("cluster-node1", 6071)); +jedisClusterNodes.add(new HostAndPort("cluster-node2", 6072)); +jedisClusterNodes.add(new HostAndPort("cluster-node3", 6073)); +JedisCluster jedis = new JedisCluster(jedisClusterNodes); +CustomStorageWrapper redis = RedisInstance.builder() + .jedisCluster(jedis) + .hashtag("{SPLITIO}") + .prefix("java:") + .build(); +SplitClientConfig config = SplitClientConfig.builder() + .customStorageWrapper(redis) + .operationMode(OperationMode.CONSUMER) + .storageMode(StorageMode.REDIS) + .setBlockUntilReadyTimeout(10000) + .enableDebug() + .build(); +SplitFactory splitFactory = SplitFactoryBuilder.build("apikey", config); +``` + + +```kotlin +// Building the Redis storage wrapper with some configurations of choice. +val jedisClusterNodes = setOf( + HostAndPort("cluster-node1", 6071), + HostAndPort("cluster-node2", 6072), + HostAndPort("cluster-node3", 6073)) + val jedis = JedisCluster(jedisClusterNodes) + val redis: CustomStorageWrapper = RedisInstance.builder() + .jedisCluster(jedis) + .hashtag("{SPLITIO}") + .prefix("java:") + .build(); + val splitConfig: SplitClientConfig = SplitClientConfig.builder() + .customStorageWrapper(redis) + .operationMode(OperationMode.CONSUMER) + .storageMode(StorageMode.REDIS) + .build() +val splitFactory: SplitFactory = SplitFactoryBuilder.build("apikey", config) +``` + + + +:::info[Redis Cluster] +The Java SDK performs multi-key operations in certain methods such as `mget` (to return values of all specified keys) or `keys` (to return all the keys that matches a particular pattern) to avoid multiple calls to Redis. Redis Cluster does not allow these operations unless you use hashtags. Hashtags ensure that multiple keys are allocated in the same hash slot. The SDK allows you to use a custom key hashtag for storing keys. If this option is missing, it uses a default hashtag of `{SPLITIO}` when `cluster` mode is specified in the configuration. Keep in mind that multi-key operations may become unavailable during a resharding of the hash slots, calls to `getTreatments`, or `manager.splitNames()`, causing `splitKeys` to fail. +::: + +## Manager + +Use the Split Manager to get a list of features available to the Split client. + +To instantiate a Manager in your code base, use the same factory that you used for your client. + + + +```java +SplitFactory splitFactory = SplitFactoryBuilder.build("YOUR_SDK_KEY"); +SplitManager manager = splitFactory.manager(); +``` + + +```kotlin +val splitFactory: SplitFactory = SplitFactoryBuilder.build("YOUR_SDK_KEY") +val manager: SplitManager = splitFactory.manager() +``` + + + +The Manager then has the following methods available: + + + +```java +/** + * Retrieves the feature flags that are currently registered with the + * SDK. + * + * @return a List of SplitView or empty. + */ +List splits(); + +/** + * Returns the feature flags registered with the SDK of this name. + * + * @return SplitView or null + */ +SplitView split(String SplitName); + +/** + * Returns the feature flag names registered with the SDK. + * + * @return a List of String containing feature flag names or empty + */ +List splitNames(); +``` + + +```kotlin +/** +* Retrieves the feature flags that are currently registered with the +* SDK. +*/ +fun splits(): List + +/** +* Returns the feature flags registered with the SDK of this name. +*/ +fun split(SplitName: String): SplitView + +/** +* Returns the feature flag names registered with the SDK. +*/ +fun splitNames(): List + +``` + + + +The `SplitView` object that you see referenced above has the following structure: + + + +```java +public class SplitView { + public String name; + public String trafficType; + public boolean killed; + public List treatments; + public long changeNumber; + public Map configs; + public List sets; + public String defaultTreatment; + public boolean impressionsDisabled; +} +``` + + +```kotlin +class SplitView( + var name: String?, + var trafficType: String?, + var killed: Boolean, + var treatments: List?, + var changeNumber: Long, + var sets: List?, + var defaultTreatment: String?, + var impressionsDisabled: boolean +) +``` + + + +## Listener + +Split SDKs send impression data back to Split servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression listener*. + +The SDK sends the generated impressions to the impression listener immediately. As a result, be careful while implementing handling logic to avoid blocking the main thread. As the second parameter, specify the size of the queue acting as a buffer (see the snippet below). + +If the impression listener is slow at processing the incoming data, the queue fills up and any subsequent impressions are dropped. + + + +```java +SplitClientConfig config = SplitClientConfig.builder() + .integrations( + IntegrationsConfig.builder() + .impressionsListener(new MyImpressionListener(), 500) + .build()) + .build(); + +SplitFactoryBuilder.build("YOUR_SDK_KEY", config).client(); + + +// Custom Impression listener class +static class MyImpressionListener implements ImpressionListener { + + @Override + public void log(Impression impression) { + // Send this data somewhere. Printing to console for now. + System.out.println(impression); + } + + @Override + public void close() { + // Do something + } +} +``` + + +```kotlin +val config: SplitClientConfig = SplitClientConfig.builder() + .integrations( + IntegrationsConfig.builder() + .impressionsListener(MyImpressionListener(), 500) + .build()) + .build() +SplitFactoryBuilder.build("YOUR_SDK_KEY", config).client() + +// Custom Impression listener class +class MyImpressionListener : ImpressionListener { + override fun log(impression: Impression) { + // Send this data somewhere. Printing to console for now. + println(impression); + } + override fun close() { + // Do something + } +} +``` + + + +## Logging + +The Java SDK uses slf4j-api for logging. If you do not provide an implementation for slf4j, you see the following error in your logs: + + + +```java +SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder". +SLF4J: Defaulting to no-operation (NOP) logger implementation +SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details. +``` + + + +You can get the SDK to log by providing a concrete implementation for SLF4J. For instance, if you are using log4j, you should import the following dependency. + + + +```java + + org.slf4j + slf4j-log4j12 + 1.7.21 + +``` + + + +If you have a log4j.properties in your classpath, the SDK log is visible. The following is an example of log4j.properties entry: + + + +```java +log4j.rootLogger=DEBUG, A1 +log4j.appender.A1=org.apache.log4j.ConsoleAppender +log4j.appender.A1.layout=org.apache.log4j.PatternLayout +log4j.appender.A1.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n +``` + + +```kotlin +log4j.rootLogger=DEBUG, A1 +log4j.appender.A1=org.apache.log4j.ConsoleAppender +log4j.appender.A1.layout=org.apache.log4j.PatternLayout +log4j.appender.A1.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n +``` + + + +The following is an example of initializing the logger object in Java: + + + +```java +import io.split.client.SplitClient; +import io.split.client.SplitClientConfig; +import io.split.client.SplitFactory; +import io.split.client.SplitFactoryBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SplitSD { + + final static Logger logger = LoggerFactory.getLogger(SplitSD.class); + public static void main(String[] args) { + SplitClientConfig config = SplitClientConfig.builder() + .setBlockUntilReadyTimeout(10000) + .enableDebug() + .build(); +``` + + +```kotlin +import io.split.client.SplitClient; +import io.split.client.SplitClientConfig; +import io.split.client.SplitFactory; +import io.split.client.SplitFactoryBuilder; +import org.slf4j.LoggerFactory + +class SplitSD { + inline fun logger() = LoggerFactory.getLogger(T::class.java) +} +``` + + + +## Thread Factory + +Since version `4.10.0`, the Java SDK provides support for Virtual Threads using the config threadFactory, instead of traditional threads. Below is an example of how to set it up: + + + +```java +import io.split.client.SplitClient; +import io.split.client.SplitClientConfig; +import io.split.client.SplitFactory; +import io.split.client.SplitFactoryBuilder; + +import java.util.concurrent.ThreadFactory; + +ThreadFactory virtualThreadFactory = Thread.ofVirtual().factory(); +SplitClientConfig config = SplitClientConfig.builder() + .setBlockUntilReadyTimeout(10000) + .threadFactory(virtualThreadFactory) + .build(); + +SplitFactory splitFactory = SplitFactoryBuilder.build("YOUR_SDK_KEY",config); +SplitClient client = splitFactory.client(); +client.blockUntilReady(); +``` + + +```kotlin +import io.split.client.SplitClient; +import io.split.client.SplitClientConfig; +import io.split.client.SplitFactory; +import io.split.client.SplitFactoryBuilder; + +val virtualThreadFactory = Thread.ofVirtual().factory(); +val config: SplitClientConfig = SplitClientConfig.builder() + .setBlockUntilReadyTimeout(10000) + .threadFactory(virtualThreadFactory) + .build() + +val splitFactory: SplitFactory = SplitFactoryBuilder.build("YOUR_SDK_KEY", config) +val client: SplitClient = splitFactory.client() +client.blockUntilReady() +``` + + + +## Integrations + +### New Relic + +The New Relic integration annotates New Relic transactions with Split feature flags information that can be used to correlate application metrics with feature flag changes. This integration is implemented as a synchronous impression listener and it can be enabled as shown below: + + + +```java +SplitClientConfig config = SplitClientConfig.builder() + .integrations( + IntegrationsConfig.builder() + .newRelicImpressionListener() + .build()) + .build(); + +SplitFactoryBuilder.build("YOUR_SDK_KEY", config).client(); +``` + + +```kotlin +val config: SplitClientConfig = SplitClientConfig.builder() + .integrations( + IntegrationsConfig.builder() + .newRelicImpressionListener() + .build()) + .build() + +SplitFactoryBuilder.build("YOUR_SDK_KEY", config).client() +``` + + + +This integration is only enabled if Split SDK detects the New Relic agent in the classpath. If the agent is not detected, the following error will be displayed in the logs (if logging is enabled): +``` +WARN [main] (IntegrationsConfig.java:72) - New Relic agent not found. Continuing without it +``` + +## Network proxy + +If you need to use a network proxy, you can configure proxies by setting the `proxyHost` and `proxyPort` options in the SDK configuration (refer [Configuration](#configuration) section for more information). The SDK reads those variables and uses them to perform the server request. + +## Advanced: WebLogic container + +WebLogic and the Split Java SDK contain a reference to Google Guava. If you are currently deploying a web application that contains our Java SDK into WebLogic, instruct the container to load Guava from the app classpath and not from the container. + +If you have an existing **weblogic.xml** file in your deployment, add: `com.google.common.*` under the `` tag. If you do not, create the file and place it under the directory `WEB-INF`. + +Here is a sample of a **weblogic.xml** file that includes the previously mentioned Guava classpath loading instruction. + + + +```java + + + /testing-java + + false + + com.google.common.* + + + + +``` + + diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/net-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/net-sdk.md new file mode 100644 index 00000000000..acc78e34f22 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/net-sdk.md @@ -0,0 +1,1029 @@ +--- +title: .NET SDK +sidebar_label: .NET SDK +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +This guide provides detailed information about our .NET SDK. All of our SDKs are open source. Go to our [.NET SDK GitHub repository](https://github.com/splitio/dotnet-client) to learn more. + +## Language Support + +This SDK supports the following .NET platform versions: + - .NET Framework 4.5 and later + - .NET Core 2.x and 3.x + - .NET 8, .NET 7, .NET 6 and .NET 5 + +## Initialization + +:::warning[Important!] +We unified the source code for Splitio and Splitio-net-core packages in one repository and there is no need to have two packages anymore. The last release of Splitio-net-core was 6.2.3. For current and future releases please use our Splitio package. +::: + +### 1. Import the SDK into your project + +Use NuGet in the command line or the Package Manager UI in Visual Studio. + +```csharp title="NuGet" +Install-Package Splitio -Version 7.10.0 +``` + +### 2. Instantiate the SDK and create a new Split client + +:::danger[If upgrading an existing SDK - Block until ready changes] +Starting version 5.0.0, .Ready is deprecated and migrated to the following implementation: +Call `SplitClient.BlockUntilReady(int milliseconds)` or `SplitManager.BlockUntilReady(int milliseconds)`. +::: + +When the SDK is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while its in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). + +To make sure the SDK is properly loaded before asking it for a treatment, block until the SDK is ready. This is done by using `.BlockUntilReady(int milliseconds)` method as part of the instantiation process of the SDK client as shown below. Do this all as a part of the startup sequence of your application. If SDK is not ready after the specified time, the SDK fails to initialize and throws a `TimeoutException` error. + +We recommend instantiating the Split factory once as a singleton and reusing it throughout your application. + +Use the code snippet below with your own API key. Configure the SDK with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split user interface, on your Admin settings page, API keys section. Select a server-side SDK API key. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. + +```csharp title="C#" +using Splitio.Services.Client.Classes; + +var config = new ConfigurationOptions(); + +var factory = new SplitFactory("YOUR_SDK_KEY", config); +var sdk = factory.Client(); + +try +{ + sdk.BlockUntilReady(10000); +} +catch (Exception ex) +{ + // log & handle +} +``` + +Now you can start asking the SDK to evaluate treatments for your customers. + +## Using the SDK + +### Basic use + +After you instantiate the SDK client, you can use the `GetTreatment` method of the SDK client to decide what version of your features your customers are served. The method requires the `FEATURE_FLAG_NAME` attribute that you want to ask for a treatment and a unique `key` attribute that corresponds to the end user that you are serving the feature to. + +Then use an if-else-if block as shown below and insert the code for the different treatments that you defined in the Split user interface. Remember the final else branch in your code to handle the client returning the [control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). + + + +```csharp +// The KEY here represents the ID of the user/account/etc you're trying to evaluate a treatment for +var treatment = sdk.GetTreatment("KEY","FEATURE_FLAG_NAME"); + +if (treatment == "on") +{ + // insert code here to show on treatment +} +else if (treatment == "off") +{ + // insert code here to show off treatment +} +else +{ + // insert your control treatment code here +} +``` + + +```csharp +// The KEY here represents the ID of the user/account/etc you're trying to evaluate a treatment for +var treatment = await sdk.GetTreatmentAsync("KEY","FEATURE_FLAG_NAME"); + +if (treatment == "on") +{ + // insert code here to show on treatment +} +else if (treatment == "off") +{ + // insert code here to show off treatment +} +else +{ + // insert your control treatment code here +} +``` + + + +### Attribute syntax + +To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK's `GetTreatment` method needs to be passed an attribute map at runtime. + +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `GetTreatment` call. These attributes are compared and evaluated against the attributes used in the Rollout plan as defined in the Split user interface to decide whether to show the `on` or `off` treatment to this account. + +The `GetTreatment` method supports five types of attributes: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: + +* **Strings:** Use type String. +* **Numbers:** Use type `long` or `int`. +* **Dates: ** Express the value for these attributes in `milliseconds since epoch` and as objects of class `DateTime`. +* **Booleans:** Use type `bool`. +* **Sets:** Use type `List`. + + + +```csharp +using Splitio.Services.Client.Classes; + +var factory = new SplitFactory("YOUR_SDK_KEY"); +var sdk = factory.Client(); + +try +{ + sdk.BlockUntilReady(10000); +} +catch (Exception ex) +{ + // log & handle +} + +var values = new List { "read", "write" }; + +var attributes = new Dictionary +{ + { "plan_type", "growth" }, + { "registered_date", System.DateTime.UtcNow }, + { "deal_size", 1000 }, + { "paying_customer", true }, + { "permissions", values } +}; + +var treatment = sdk.GetTreatment("KEY", "FEATURE_FLAG_NAME", attributes); + +if (treatment == "on") +{ + // insert code here to show on treatment +} +else if (treatment == "off") +{ + // insert code here to show off treatment +} +else +{ + // insert your control treatment code here +} +``` + + +```csharp +using Splitio.Services.Client.Classes; + +var factory = new SplitFactory("YOUR_SDK_KEY"); +var sdk = factory.Client(); + +try +{ + sdk.BlockUntilReady(10000); +} +catch (Exception ex) +{ + // log & handle +} + +var values = new List { "read", "write" }; + +var attributes = new Dictionary +{ + { "plan_type", "growth" }, + { "registered_date", System.DateTime.UtcNow }, + { "deal_size", 1000 }, + { "paying_customer", true }, + { "permissions", values } +}; + +var treatment = await sdk.GetTreatmentAsync("KEY", "FEATURE_FLAG_NAME", attributes); + +if (treatment == "on") +{ + // insert code here to show on treatment +} +else if (treatment == "off") +{ + // insert code here to show off treatment +} +else +{ + // insert your control treatment code here +} +``` + + + +### Multiple evaluations at once + +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `GetTreatments` from the Split client to do this. +* `GetTreatments`: Pass a list of the feature flag names you want treatments for. +* `GetTreatmentsByFlagSet`: Evaluate all flags that are part of the provided set name and are cached on the SDK instance. +* `GetTreatmentsByFlagSets`: Evaluate all flags that are part of the provided set names and are cached on the SDK instance. + + + +```csharp +var featureFlagNames = new List { "FEATURE_FLAG_NAME_1", "FEATURE_FLAG_NAME_2" }; +var result = client.GetTreatments("KEY", featureFlagNames); +``` + + +```csharp +var result = client.GetTreatmentsByFlagSet("KEY", "backend"); +``` + + +```csharp +var flagSets = new List { "backend", "server_side" }; +var result = client.GetTreatmentsByFlagSets("KEY", flagSets); +``` + + +```csharp +var featureFlagNames = new List { "FEATURE_FLAG_NAME_1", "FEATURE_FLAG_NAME_2" }; +var result = await client.GetTreatmentsAsync("KEY", featureFlagNames); +``` + + +```csharp +var result = await client.GetTreatmentsByFlagSetAsync("KEY", "backend"); +``` + + +```csharp +var flagSets = new List { "backend", "server_side" }; +var result = await client.GetTreatmentsByFlagSetsAsync("KEY", flagSets); +``` + + + +You can also use the [Split Manager](#manager) to get all of your treatments at once. + +### Get Treatments with Configurations + +To [leverage dynamic configurations with your treatments](https://help.split.io/hc/en-us/articles/360026943552), you should use the `GetTreatmentWithConfig` method. + +This method returns an object containing the treatment and associated configuration. + +The config element is a stringified version of the configuration JSON defined in the Split user interface. If there is no configuration defined for a treatment, the SDK returns `null` for the config parameter. + +This method takes the exact same set of arguments as the standard `GetTreatment` method. See below for examples on proper usage: + + + +```csharp +var featureFlagResult = splitClient.GetTreatmentWithConfig("KEY", "FEATURE_FLAG_NAME"); +var config = JsonConvert.DeserializeObject(featureFlagResult.Config); +var treatment = featureFlagResult.Treatment; +``` + + +```csharp +var featureFlagResult = await splitClient.GetTreatmentWithConfigAsync("KEY", "FEATURE_FLAG_NAME"); +var config = JsonConvert.DeserializeObject(featureFlagResult.Config); +var treatment = featureFlagResult.Treatment; +``` + + + +If you need to get multiple evaluations at once, you can also use the `GetTreatmentsWithConfig` methods. These methods take the exact same arguments as the [GetTreatments](#multiple-evaluations-at-once) methods but return a mapping of feature flag names to SplitResult instead of strings. Example usage below: + + + +```csharp +var featureFlagNames = new List { "FEATURE_FLAG_NAME_1", "FEATURE_FLAG_NAME_2" }; +var featureFlagResults = splitClient.GetTreatmentsWithConfig("KEY", featureFlagNames); +``` + + +```csharp +var featureFlagResults = splitClient.GetTreatmentsWithConfigByFlagSet("KEY", "backend"); +``` + + +```csharp +var flagSets = new List { "backend", "server_side" }; +var featureFlagResults = splitClient.GetTreatmentsWithConfigByFlagSets("KEY", flagSets); +``` + + +```csharp +var featureFlagNames = new List { "FEATURE_FLAG_NAME_1", "FEATURE_FLAG_NAME_2" }; +var featureFlagResults = await splitClient.GetTreatmentsWithConfigAsync("KEY", featureFlagNames); +``` + + +```csharp +var featureFlagResults = await splitClient.GetTreatmentsWithConfigByFlagSetAsync("KEY", "backend"); +``` + + +```csharp +var flagSets = new List { "backend", "server_side" }; +var featureFlagResults = await splitClient.GetTreatmentsWithConfigByFlagSetsAsync("KEY", flagSets); +``` + + + +### Shutdown + +Call the `.destroy()` method before letting a process using the SDK exit, as this method gracefully shuts down the Split SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. + +If a manual shutdown is required, call the `client.Destroy()` method. + + + +```csharp +client.Destroy(); +``` + + +```csharp +await client.DestroyAsync(); +``` + + + +:::warning[Important!] +A call to the `destroy()` method also destroys the factory object. When creating new client instance, first create a new factory instance. +::: + +## Track + +Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users’ actions and metrics. + +[Learn more](https://help.split.io/hc/en-us/articles/360020585772) about using track events in feature flags. + +In the examples below, you can see that the `.track()` method can take up to four arguments. The proper data type and syntax for each are: + +* **key:** The `key` variable used in the `GetTreatment` call and firing this track event. The expected data type is **String**. +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: + * Contains 63 characters or fewer. + * Starts with a letter or number. + * Contains only letters, numbers, hyphen, underscore, or period. + * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` +* **VALUE:** (Optional) The value to be used in creating the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is **Integer** or **Double**. +* **PROPERTIES:** (Optional) A map of key value pairs that can be used to filter your metrics. Learn more about event property capture [in the Events guide](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties). Split currently supports three types of properties: strings, numbers, and booleans. + +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK can successfully queue the event to be sent back to Split's servers on the next event post. The SDK returns `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method has been provided. + +In case a bad input is provided, refer to the [Events](https://help.split.io/hc/en-us/articles/360020585772-Track-events) guide for more information about our SDK's expected behavior. + + + +```csharp +// If you would like to send an event without a value +bool success = client.Track("KEY", "TRAFFIC_TYPE", "EVENT_TYPE"); +// Example +bool success = client.Track("john@doe.com", "user", "page_load_time"); + +// If you would like to associate a value to an event +bool success = client.Track("KEY", "TRAFFIC_TYPE", "EVENT_TYPE", VALUE); +// Example +bool success = client.Track("john@doe.com", "user", "page_load_time", 83.334); + +// If you would like to associate a value and properties to an event +bool trackEvent = client.Track("KEY", "TRAFFIC_TYPE", "EVENT_TYPE", VALUE, {PROPERTIES}); +// Example +var properties = new Dictionary +{ + { "package", "premium" }, + { "admin", true }, + { "discount", 50 } +}; + +bool trackEvent = client.Track("john@doe.com", "user", "page_load_time", 83.334, properties); + +// If you would like to associate just properties to an event +bool trackEvent = client.Track("KEY", "TRAFFIC_TYPE", "EVENT_TYPE", properties: {PROPERTIES}); +// Example +var properties = new Dictionary +{ + { "package", "premium" }, + { "admin", true }, + { "discount", 50 } +}; + +bool trackEvent = client.Track("john@doe.com", "user", "page_load_time", properties: properties); +``` + + +```csharp +// If you would like to send an event without a value +bool success = await client.TrackAsync("KEY", "TRAFFIC_TYPE", "EVENT_TYPE"); +// Example +bool success = await client.TrackAsync("john@doe.com", "user", "page_load_time"); + +// If you would like to associate a value to an event +bool success = await client.TrackAsync("KEY", "TRAFFIC_TYPE", "EVENT_TYPE", VALUE); +// Example +bool success = await client.TrackAsync("john@doe.com", "user", "page_load_time", 83.334); + +// If you would like to associate a value and properties to an event +bool trackEvent = await client.TrackAsync("KEY", "TRAFFIC_TYPE", "EVENT_TYPE", VALUE, {PROPERTIES}); +// Example +var properties = new Dictionary +{ + { "package", "premium" }, + { "admin", true }, + { "discount", 50 } +}; + +bool trackEvent = await client.TrackAsync("john@doe.com", "user", "page_load_time", 83.334, properties); + +// If you would like to associate just properties to an event +bool trackEvent = await client.TrackAsync("KEY", "TRAFFIC_TYPE", "EVENT_TYPE", properties: {PROPERTIES}); +// Example +var properties = new Dictionary +{ + { "package", "premium" }, + { "admin", true }, + { "discount", 50 } +}; + +bool trackEvent = await client.TrackAsync("john@doe.com", "user", "page_load_time", properties: properties); +``` + + + +## Configuration + +The SDK has a number of knobs for configuring performance. Each knob is tuned to a reasonable default. However, you can override the value while instantiating the SDK. The parameters available for configuration are shown below. + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| FeaturesRefreshRate | The SDK polls Split servers for changes to feature flags at this rate (in seconds). | 5 seconds | +| SegmentsRefreshRate | The SDK polls Split servers for changes to segments at this rate (in seconds). | 60 seconds | +| ImpressionsRefreshRate | The treatment log captures which customer saw what treatment (on, off, etc) at what time. This log is periodically flushed back to Split servers. This configuration controls how quickly the cache expires after a write (in seconds). | 30 seconds | +| TelemetryRefreshRate | The SDK caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers (in seconds). | 3600 seconds | +| ConnectionTimeout | HTTP client connection timeout (in ms). | 15000ms | +| ReadTimeout | HTTP socket read timeout (in ms). | 15000ms | +| LabelsEnabled | Enable/disable labels from being sent to the Split backend. Labels may contain sensitive information. | true | +| EventsFirstPushWindow | The SDK collects the events generated by the customer. This setting controls the number of seconds to wait for sending the events to the Split servers (in seconds) after the SDK is built. | 10 seconds | +| EventsPushRate | The SDK collects the events generated by the customer. This setting controls how frequently the events are sent to the Split servers (in seconds) after the first push. | 60 seconds | +| EventsQueueSize | The SDK collects the events generated by the customer. This setting controls how many events are stored locally before sending them to the Split servers (for standalone mode only). | 500 | +| IPAddressesEnabled | Disable machine IP and Hostname from being sent to Split backend. IP and Hostname may contain sensitive information. | true | +| StreamingEnabled | Boolean flag to enable the streaming service as default synchronization mechanism. In the event of an issue with streaming, the SDK will fallback to the polling mechanism. If false, the SDK will poll for changes as usual without attempting to use streaming. | true | +| ImpressionsMode | Defines how impressions are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Split; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, all impressions are queued and sent to Split; this is useful for validations. Use DEBUG mode when you want every impression to be logged in Split user interface when trying to debug your SDK setup. This setting does not impact the impression listener which receives all generated impressions locally. | Optimized | +| ProxyHost | The name of the proxy host. | string.empty | +| ProxyPort | The port number on Host to use. | 0 (not set) | +| FlagSetsFilter | This setting allows the SDK to only synchronize the feature flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the SDK instance, bringing all the benefits from a reduced payload. | null | + +To set each of the parameters above, use the following syntax: + +```csharp title="C#" +var config = new ConfigurationOptions +{ + ReadTimeout = 15000, + ConnectionTimeout = 15000, + FlagSetsFilter = new List { "backend", "server_side" } +}; + +var factory = new SplitFactory("YOUR_SDK_KEY", config); +var sdk = factory.Client(); + +try +{ + sdk.BlockUntilReady(10000); +} +catch (Exception ex) +{ + // log & handle +} +``` + +## Sharing state: Redis integration + +**Configuring this Redis integration section is optional for most setups. Read below to determine if it might be useful for your project** + +By default, the Split client stores the state it needs to compute treatments (rollout plans, segments, and so on) in memory. As a result, it is easy to get set up with Split: instantiate a client and start using it. + +This simplicity hides one important detail that is worth exploring. Because each Split client downloads and stores state separately, a change in a feature flag is picked up by every client on its own schedule. If a customer issues back-to-back requests that are served by two different machines behind a load balancer, the customer can see different treatments for the same feature flag because one Split client may not have picked up the latest change. This drift in clients is natural and usually ignorable as long as each client sets an aggressive value for `FeaturesRefreshRate` and `SegmentsRefreshRate`. You can learn more about setting these rates in the [Configuration section](#configuration) below. + +However, if your application requires a total guarantee that Split clients across your entire infrastructure pick up a change in a feature flag at the exact same time, then the only way to ensure that is to externalize the state of the Split client in a data store hosted on your infrastructure. + +We currently support Redis for this external data store. + +To use the .Net SDK with Redis, you need to set up the Split Synchronizer and instantiate the SDK in Consumer mode. + +### Producer + +Refer to our [Split Synchronizer](https://help.split.io/hc/en-us/articles/360019686092) documents and follow the steps there to get everything set to sync data to your Redis cache. After you do that, come back to set up the Consumer. + +### Consumer + +First, import Splitio.Redis NuGet package into your project. + +Use NuGet in the command line or the Package Manager UI in Visual Studio. + +```csharp title="NuGet" +Install-Package Splitio.Redis -Version 7.10.0 +``` + +To initiate an SDK with support for Redis as consumer mode, use the following code snippet: + +```csharp title="C#" +using Splitio.Services.Client.Classes; + +var cacheAdapterConfigurationOptions = new CacheAdapterConfigurationOptions +{ + Type = AdapterType.Redis, + Host = "localhost", + Port = "6379", + Password = "", + Database = 2, + ConnectTimeout = 5000, + ConnectRetry = 3, + SyncTimeout = 1000, + UserPrefix = "my_user_prefix", + PoolSize = 1 +}; + +var config = new ConfigurationOptions +{ + Mode = Mode.Consumer, + CacheAdapterConfig = cacheAdapterConfigurationOptions +}; + +try +{ + sdk.BlockUntilReady(10000); +} +catch (Exception ex) +{ + // log & handle +} +``` + +Available modes are *standalone* (default, for in-memory cache) and *consumer* (for Redis cache). + +### SSL support for Redis connections + +This is a basic snippet to set it up + +```csharp title="C#" +using Splitio.Services.Client.Classes; +using Splitio.Domain; + +var tlsConfig = new TlsConfig(ssl: true); +var cacheAdapterConfigurationOptions = new CacheAdapterConfigurationOptions +{ + // .... + TlsConfig = tlsConfig +}; +``` + +There are two more functions that you can optionally set up, for an advanced use case, that we provide to the library underneath if set: +* If you want to be responsible for validating the certificate supplied by the remote party, you should define the CertificateValidationFunc. [Doc](https://github.com/StackExchange/StackExchange.Redis/blob/main/src/StackExchange.Redis/ConfigurationOptions.cs#L158) +* If you want to be responsible for selecting the certificate used for authentication, you should define CertificateSelectionFunc. [Doc](https://github.com/StackExchange/StackExchange.Redis/blob/main/src/StackExchange.Redis/ConfigurationOptions.cs#L151) + +```csharp title="C#" +using Splitio.Services.Client.Classes; +using Splitio.Domain; + +var tlsConfig = new TlsConfig(ssl: true) +{ + CertificateValidationFunc = CertificateValidation, + CertificateSelectionFunc = CertificateSelection +}; + +var cacheAdapterConfigurationOptions = new CacheAdapterConfigurationOptions +{ + // .... + TlsConfig = tlsConfig +}; +``` + +```csharp title="C#" +private X509Certificate2 CertificateSelection(object sender, string targetHost, X509CertificateCollection localCertificates, X509Certificate remoteCertificate, string[] acceptableIssuers) +{ + // your custom code +} + +private bool CertificateValidation(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) +{ + // your custom code +} +``` + +### Redis cluster support + +The Split .NET SDK version **7.10.0 and above** supports Redis with [Cluster](https://redis.io/topics/cluster-spec). + +To initiate the SDK with support for Redis Cluster, use the following code snippet: + +```csharp title="C#" +using Splitio.Services.Client.Classes; + +var clusterNodes = new Splitio.Domain.ClusterNodes +( + new List() { "RedisNode1:6379", "RedisNode2:6380", "RedisNode3:6381", ... }, + "{SPLITIO}" // KeyHashTag added to user prefix +); +var cacheAdapterConfigurationOptions = new CacheAdapterConfigurationOptions +{ + Type = AdapterType.Redis, + RedisClusterNodes = clusterNodes, + Password = "", + Database = 2, + ConnectTimeout = 5000, + ConnectRetry = 3, + SyncTimeout = 1000, + UserPrefix = "my_user_prefix", + PoolSize = 1 +}; + +var config = new ConfigurationOptions +{ + Mode = Mode.Consumer, + CacheAdapterConfig = cacheAdapterConfigurationOptions +}; + +var factory = new SplitFactory("YOUR_SDK_KEY", config); +var sdk = factory.Client(); + +try +{ + sdk.BlockUntilReady(10000); +} +catch (Exception ex) +{ + // log & handle +} +``` + +:::warning[The KeyHashTag Parameter] +The KeyHashTag is a required parameter. If left empty, the SDK will by default use "\{SPLITIO\}" as the KeyHashTag value. The KeyHashTag value is added to the user prefix to improve SDK performance in Redis Cluster. +You should use the same KeyHashTag value in the [Split Synchronizer](https://help.split.io/hc/en-us/articles/360019686092-Split-Synchronizer) app synching to the same Redis cluster. +::: + +## Localhost mode + +For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the Split SDK can be started in **localhost** mode (aka off-the-grid mode). In this mode, the SDK neither polls nor updates Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the feature flags. + +To use the SDK in localhost mode, replace the SDK Key with "localhost", as shown in the example below: + +Since version 4.1.2, our SDK supports a new type of localhost feature flag definition file, using the YAML format. +This new format allows the user to map different keys to different treatments within a single feature flag, and also add configurations to them. +The new format is a list of single-key maps (one per mapping feature_flag-keys-config), defined as follows: + +```yaml title="YAML" +## - feature_name: +## treatment: "treatment_applied_to_this_entry" +## keys: "single_key_or_list" +## config: "{\"desc\" : \"this applies only to ON treatment\"}" + +- my_feature: + treatment: "on" + keys: "key" + config: "{\"desc\" : \"this applies only to ON treatment\"}" +- some_other_feature: + treatment: "off" +- my_feature: + treatment: "off" +- other_feature: + treatment: "off" + keys: ["key_1", "key_2"] + config: "{\"desc\" : \"this overrides multiple keys and returns off treatment for those keys\"}" +``` + +In the example above, we have four entries: + * The first entry defines that for feature flag `my_feature`, the key `key` returns the treatment `on` and the `on` treatment is tied to the configuration `{"desc" : "this applies only to ON treatment"}`. + * The second entry defines that the feature flag `some_other_feature` always returns the `off` treatment and no configuration. + * The third entry defines that `my_feature` always returns `off` for all keys that don't match another entry (in this case, any key other than `key`). + * The fourth entry shows how an example to override a treatment for a set of keys. + + +Once you've defined your yaml file, you can instantiate the SDK in localhost mode as detailed below: + +```csharp title="C#" +var config = new ConfigurationOptions +{ + LocalhostFilePath = "FILE_PATH" +}; + +var factory = new SplitFactory("localhost", config); +var sdk = factory.Client(); + +try +{ + sdk.BlockUntilReady(10000); +} +catch (Exception ex) +{ + // log & handle +} +``` + +Split SDK maintains backward compatibility by the legacy file (.split), now deprecated. + +```csharp title="C#" +var config = new ConfigurationOptions +{ + LocalhostFilePath = "$HOME/.split" +}; + +var factory = new SplitFactory("localhost", config); +var client = factory.Client(); + +try +{ + sdk.BlockUntilReady(10000); +} +catch (Exception ex) +{ + // log & handle +} +``` + +In this mode, the SDK loads a mapping of feature flag name to treatment from a file at `$HOME/.split`. For a given feature flag, the treatment specified in the file is returned for every customer. + +`GetTreatment` calls for a feature flag only return the one treatment that you defined in the file. You can then change the treatment as necessary for your testing in the file. Any feature flag that is not provided in the `features` map returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment) if the SDK is asked to evaluate them. + +The format of this file is two columns separated by a whitespace. The left column is the feature flag name, and the right column is the treatment name. Here is a sample `.split` file. + +```bash title="Shell" +reporting_v2 on ## sdk.getTreatment(*, reporting_v2) will return 'on' + +double_writes_to_cassandra off + +new-navigation v3 +``` + +## Manager + +Use the Split Manager to get a list of feature flags available to the Split client. + +To instantiate a Manager in your code base, use the same factory that you used for your client. + +```csharp title="Manager" +var factory = new SplitFactory("YOUR_SDK_KEY"); +var splitManager = factory.Manager(); +``` + +The Manager has the following methods available. + + + +```csharp +/** +* Retrieves the feature flags that are currently registered with the +* SDK. +* +* @return a List of SplitView or empty. +*/ +List Splits(); + +/** +* Returns the feature flags registered with the SDK of this name. +* +* @return SplitView or null +*/ +SplitView Split(string splitName); + +/** +* Returns the names of feature flags registered with the SDK. +* +* @return a List of String (feature flag names) or empty +*/ +List SplitNames(); +``` + + +```csharp +/** +* Retrieves the feature flags that are currently registered with the +* SDK. +* +* @return a List of SplitView or empty. +*/ +Task> SplitsAsync(); + +/** +* Returns the feature flags registered with the SDK of this name. +* +* @return SplitView or null +*/ +Task SplitAsync(string splitName); + +/** +* Returns the names of feature flags registered with the SDK. +* +* @return a List of String (feature flag names) or empty +*/ +Task> SplitNamesAsync(); +``` + + + +The `SplitView` object that you see referenced above has the following structure. + +```csharp title="SplitView" +public class SplitView +{ + public string name { get; } + public string trafficType { get; } + public bool killed { get; } + public List treatments { get; } + public long changeNumber { get; } + public Dictionary configs { get; } + public string defaultTreatment { get; } + public List sets { get; } +} +``` + +## Listener + +Split SDKs send impression data back to Split servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an impression listener. + +The SDK sends the generated impressions to the impression listener right away. + +To create an impression listener, you need to implement an `IImpressionListener` interface. + +```csharp title="Listener" +public class CustomImpressionListener: IImpressionListener +{ + ... + + public void Log(KeyImpression impression) + { + //Implement your custom code + } +} +``` + +Then add a configuration to attach to it. + +```csharp title="Configuration" +... +configurations.ImpressionListener = new CustomImpressionListener(); +... + +var factory = new SplitFactory("YOUR_SDK_KEY", configurations); +var sdk = factory.Client(); + +try +{ + sdk.BlockUntilReady(10000); +} +catch (Exception ex) +{ + // log & handle +} +``` + +## Proxy + +If you need to use a proxy, you can configure proxies by setting the environment variables `HTTP_PROXY` and `HTTPS_PROXY`. The SDK reads those variables and uses them to perform the server request. [Documentation](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.defaultproxy?view=net-7.0) + +Also you can configure proxies by setting the ProxyHost and ProxyPort properties in the SDK configuration (refer [Configuration](#configuration) section for more information). The SDK uses those properties, with higher precedence than environment variables, to perform the server request. [Documentation](https://learn.microsoft.com/en-us/dotnet/api/system.net.webproxy.-ctor?view=net-7.0#system-net-webproxy-ctor(system-string-system-int32)) + +## Logging + +### SDK logging + +The .NET SDK uses Common.Logging for logging. It allows you to configure different adapters such as log4net or NLog, and you can also write your own adapter by implementing an `ILoggerFactoryAdapter` interface. For more details, go [here](http://netcommon.sourceforge.net/docs/2.1.0/reference/html/ch01.html). + +The following is an example of how to configure NLog and its adapter. + +```csharp title="C#" +var configLog = new NLog.Config.LoggingConfiguration(); +var fileTarget = new NLog.Targets.FileTarget(); +configLog.AddTarget(“file”, fileTarget); +fileTarget.FileName = @“mylog.log”; +fileTarget.ArchiveFileName = “ANYFILENAME”; +fileTarget.LineEnding = NLog.Targets.LineEndingMode.CRLF; +fileTarget.Layout = “${longdate} ${level: uppercase = true} ${logger} - ${message} - ${exception:format=tostring}“; +fileTarget.ConcurrentWrites = true; +fileTarget.CreateDirs = true; +fileTarget.ArchiveNumbering = NLog.Targets.ArchiveNumberingMode.Date; +var rule = new NLog.Config.LoggingRule(“*”, NLog.LogLevel.Debug, fileTarget); +configLog.LoggingRules.Add(rule); +NLog.LogManager.Configuration = configLog; + +System.Collections.Specialized.NameValueCollection properties = new System.Collections.Specialized.NameValueCollection(); +properties[“configType”] = “INLINE”; +Common.Logging.LogManager.Adapter = new MyAdapter(); +``` + +The .NET Core SDK uses [Microsoft.Extensions.Logging](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging?view=dotnet-plat-ext-3.0) that works well with a variety of built-in and third-party logging providers. + +The following example shows how to include the Split SDK logs in only one line by updating your Startup class. + +```csharp title="C#" +//... +using Microsoft.Extensions.Logging; + +public class Startup +{ + //... + + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + //... + + loggerFactory.AddSplitLogs(); + } +} +``` + +In the following example we use [Serilog](https://github.com/serilog/serilog-extensions-logging-file) to configure text file logging. Of course you can use the tool of your preference. + +```csharp title="C#" + loggerFactory + .AddSplitLogs() + .AddFile("Logs/myapp-{Date}.txt", LogLevel.Debug); // calling Serilog AddFile method +``` + +### Enable debug logging using Log4net library in ASP .NET Core App + +The following is an example that enables debug logging using the Log4net library in the ASP .NET Core app. + +**Environment** + +log4net 2.0.12 + +.NET Core 3.1 + +To use this example, do the following: + +1. [Download the project](https://github.com/splitio/split-dotnet-debug-log-examples/tree/main/SplitLog4netExample_NETCore) and open the project from Visual Studio. +2. Open the SplitInitializer.cs file and replace the "SDK API KEY" text with the server side SDK KEY. +3. Optionally edit the log4net.config file to change the log file path, name or format. + +### Enable debug logging using Log4net library + +The following is an example that enables debug logging using the Log4net library in .NET framework console app. + +**Environment** + +log4net 2.0.8 + +.NET Framework 4.8 + +To use this example, do the following: + +1. [Download the project](https://github.com/splitio/split-dotnet-debug-log-examples/tree/main/SplitLog4netExample_NETFramework) and open the project from Visual Studio. +2. Open the Program.cs file and update the apikey variable with the server side SDK KEY. +3. Optionally edit the log4net.config file to change the log file path, name, or format. + +### Custom logging + +To use a custom logger, implement the interface `ISplitLogger` which is defined as follows: +```csharp title="C#" +public interface ISplitLogger +{ + void Debug(string message, Exception exception); + void Debug(string message); + void Error(string message, Exception exception); + void Error(string message); + void Info(string message, Exception exception); + void Info(string message); + void Trace(string message, Exception exception); + void Trace(string message); + void Warn(string message, Exception exception); + void Warn(string message); + bool IsDebugEnabled { get; } +} +``` + +The following is an example of how config the customer logger in the SDK: + +:::info[Note] +It's the developer's responsibility to ensure that the following logger methods don't crash, and can handle logging levels if a custom logger is user. +::: + +```csharp title="C#" +using Microsoft.Extensions.Logging; + +public class CustomLoggerImplementation : ISplitLogger +{ + public void Debug(string message, Exception exception) + { + // your code + } + + // Should implement interface. +} + +// ... + +var config = new ConfigurationOptions +{ + ... + Logger = new CustomLoggerImplementation() +}; + +var factory = new SplitFactory("YOUR_SDK_KEY", config); +var sdk = factory.Client(); +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/nodejs-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/nodejs-sdk.md new file mode 100644 index 00000000000..bd8aa3c6f6c --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/nodejs-sdk.md @@ -0,0 +1,1153 @@ +--- +title: Node.js SDK +sidebar_label: Node.js SDK +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +This guide provides detailed information about our Node.js SDK. All of our SDKs are open source. Go to our [Node.js SDK GitHub repository](https://github.com/splitio/javascript-client) to learn more. + +## Language support + +The JavaScript SDK supports Node.js version 14.x or later. + +## Initialization + +Set up Split in your code base with two simple steps. + +### 1. Import the SDK into your project + +The SDK is published using `npm`, so it's fully integrated with your workflow. + +```bash title="NPM" +npm install --save @splitsoftware/splitio +``` + +:::warning[If using Synchronizer with Redis - Synchronizer 2.x required after SDK Version 10.6.0] +Since version 2.0.0 of the split-synchronizer, we use a more efficient scheme to store impressions in Redis. This approach is faster and easier on your Redis instances, since it yields better throughput of impressions to the backend. If you use this SDK with the Synchronizer in Redis or Proxy mode, you will need the newest versions of our Split Synchronizer. It is recommended that once you're using SDK versions compatible with Split-Sync 2.0 on all your applications pointing to the redis instance maintained by the Split-Sync, you disable backwards compatibility (this is as easy as changing a parameter to `true` on the JSON config or an environment variable to `on` if you're using the docker image). +::: + +### 2. Instantiate the SDK and create a new Split client + + + +```javascript +var SplitFactory = require('@splitsoftware/splitio').SplitFactory; + +var factory = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY' + } +}); + +var client = factory.client(); +``` + + +```javascript +import { SplitFactory } from '@splitsoftware/splitio'; + +const factory: SplitIO.ISDK = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY' + } +}); + +const client: SplitIO.IClient = factory.client(); +``` + + + +:::warning +**Updating to Node.js SDK version 11** + +While Split Node.js SDK previously supported Node.js v6 and above, the SDK now requires Node.js v14 or above. + +**Updating to Node.js SDK version 10** + +We changed our module system to ES modules and now we are exposing an object with a SplitFactory property. That property points to the same factory function that we were returning in the previous versions. Take a look at the snippet above to see the code. +::: + +:::danger[Running in the Browser?] +Refer to our [JavaScript SDK Setup](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#1-import-the-sdk-into-your-project) as the bundles and API are slightly different. +::: + +:::info[Notice for TypeScript] +With the SDK package on NPM, you get the SplitIO namespace, which contains useful types and interfaces for you to use. + +Feel free to dive in to the declaration files if IntelliSense is not enough! +::: + +We recommend instantiating the Split factory once as a singleton and reusing it throughout your application. + +Configure the SDK with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split user interface, on your Admin settings page, API keys section. Select a server-side SDK API key. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. + +Use the Split client to evaluate treatments. + +## Using the SDK + +### Basic use + +When the SDK is instantiated, it kicks off background jobs to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds. While the SDK is in this intermediate state, if it is asked to evaluate which treatment to show to the logged in customer for a specific feature flag, it may not have data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). + +To make sure the SDK is properly loaded before asking it for a treatment, block until the SDK is ready (as shown below). We set the client to listen for the `SDK_READY` event triggered by the SDK before asking for an evaluation. + +When the `SDK_READY` event fires, you can use the `getTreatment` method to return the proper treatment based on the `key` and `FEATURE_FLAG_NAME` attributes you provided. + +Then use an if-else-if block as shown below and insert the code for the different treatments that you defined in the Split user interface. Remember the final else branch in your code to handle the client returning the [control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). + + + +```javascript +client.on(client.Event.SDK_READY, function() { + // The key here represents the ID of the user/account/etc you're trying to evaluate a treatment for + var treatment = client.getTreatment('key', 'FEATURE_FLAG_NAME'); + + if (treatment == 'on') { + // insert code here to show on treatment + } else if (treatment == 'off') { + // insert code here to show off treatment + } else { + // insert your control treatment code here + } +}); +``` + + +```javascript +client.on(client.Event.SDK_READY, () => { + // The key here represents the ID of the user/account/etc you're trying to evaluate a treatment for + const treatment: SplitIO.Treatment = + client.getTreatment('key', 'FEATURE_FLAG_NAME'); + + if (treatment == 'on') { + // insert code here to show on treatment + } else if (treatment == 'off') { + // insert code here to show off treatment + } else { + // insert your control treatment code here + } +}); +``` + + + +### Attribute syntax + +To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK's `getTreatment` method needs to be passed an attribute map at runtime. + +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in the Split user interface to decide whether to show the `on` or `off` treatment to this account. + +The `getTreatment` method has a number of variations that are described below. Each of these additionally has a variation that takes an attributes argument, which can defines attributes of the following types: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: + +* **Strings:** Use type String. +* **Numbers:** Use type Number. +* **Dates: ** Express the value for these attributes in `milliseconds since epoch` and as objects of class `DateTime`. +* **Booleans:** Use type Boolean. +* **Sets:** Use type Array. + + + + +```javascript +var attributes = { + // date attributes are handled as `millis since epoch` + registered_date: new Date('YYYY-MM-DDTHH:mm:ss.sssZ').getTime(), + // this string will be compared against a list called `plan_type` + plan_type: 'growth', + // this number will be compared against a const value called `deal_size` + deal_size: 10000, + // this boolean will be compared against a const value called `paying_customer` + paying_customer: true, + // this array will be compared against a set called `permissions` + permissions: [‘read’, ‘write’] +}; + +var treatment = client.getTreatment('key', 'FEATURE_FLAG_NAME', attributes); + +if (treatment === 'on') { + // insert on code here +} else if (treatment === 'off') { + // insert off code here +} else { + // insert control code here +} +``` + + +```javascript +const attributes: SplitIO.Attributes = { + // date attributes are handled as `millis since epoch` + registered_date: new Date('YYYY-MM-DDTHH:mm:ss.sssZ').getTime(), + // this string will be compared against a list called `plan_type` + plan_type: 'growth', + // this number will be compared agains a const value called `deal_size` + deal_size: 10000, + // this array will be compared against a set called `permissions` + permissions: [‘read’, ‘write’] +}; + +const treatment: SplitIO.Treatment = + client.getTreatment('key', 'FEATURE_FLAG_NAME', attributes); + +if (treatment === 'on') { + // insert on code here +} else if (treatment === 'off') { + // insert off code here +} else { + // insert control code here +} +``` + + + +You can pass your attributes in the same way to the `client.getTreatments` method. + +### Multiple evaluations at once + +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` from the Split client to do this. +* `getTreatments`: Pass a list of the feature flag names you want treatments for. +* `getTreatmentsByFlagSet`: Evaluate all flags that are part of the provided set name and are cached on the SDK instance. +* `getTreatmentsByFlagSets`: Evaluate all flags that are part of the provided set names and are cached on the SDK instance. + + + + +```javascript +// Getting treatments by feature flag names +var featureFlagNames = ['FEATURE_FLAG_NAME_1', 'FEATURE_FLAG_NAME_2']; +var treatments = client.getTreatments('key', featureFlagNames); + +// Getting treatments by set +treatments = client.getTreatmentsByFlagSet('key', 'backend'); + +// Getting treatments for the union of multiple sets +var flagSets = ['backend', 'server_side']; +treatments = client.getTreatmentsByFlagSets('key', flagSets); + +// treatments have the following form: +// { +// FEATURE_FLAG_NAME_1: 'on', +// FEATURE_FLAG_NAME_2: 'visa' +// } +``` + + + +```javascript +// Getting treatments by feature flag names +const featureFlagNames = ['FEATURE_FLAG_NAME_1', 'FEATURE_FLAG_NAME_1']; +let treatments: SplitIO.Treatments = client.getTreatments('key', featureFlagNames); + +// Getting treatments by set +treatments = client.getTreatmentsByFlagSet('key', 'backend'); + +// Getting treatments for the union of multiple sets +const flagSets = ['backend', 'server_side']; +treatments = client.getTreatmentsByFlagSets('key', flagSets); + +// treatments have the following form: +// { +// FEATURE_FLAG_NAME_1: 'on', +// FEATURE_FLAG_NAME_2: 'visa' +// } +``` + + + +You can also use the [Split Manager](#manager) to get all of your treatments at once. + +:::warning[Working with sync and async storage] +If your code runs with both types of storage, read [Working with both sync and async storage](#working-with-both-sync-and-async-storage). +::: + +### Get treatments with configurations + +To [leverage dynamic configurations with your treatments](https://help.split.io/hc/en-us/articles/360026943552), you should use the `getTreatmentWithConfig` method. + +This method will return an object containing the treatment and associated configuration. + +The config element will be a stringified version of the configuration JSON defined in the Split user interface. If there is no configuration defined for a treatment, the SDK will return `null` for the config parameter. + +This method takes the exact same set of arguments as the standard `getTreatment` method. See below for examples on proper usage: + + + + +```javascript +var flagResult = client.getTreatmentWithConfig('user_id', 'FEATURE_FLAG_NAME', attributes); +var configs = JSON.parse(flagResult.config); +var treatment = flagResult.treatment; +``` + + +```javascript +const flagResult: SplitIO.TreatmentWithConfig = client.getTreatmentWithConfig('user_id', 'FEATURE_FLAG_NAME', attributes); +const configs: any = JSON.parse(flagResult.config); +const treatment: SplitIO.Treatment = flagResult.treatment; +``` + + + +If you need to get multiple evaluations at once, you can also use the `getTreatmentsWithConfig` methods. These methods take the exact same arguments as the [getTreatments](#multiple-evaluations-at-once) methods but return a mapping of feature flag names to TreatmentResults instead of strings. Example usage below: + + + + +```javascript +// Getting treatments by feature flag names +var flagNames = ['FEATURE_FLAG_NAME_1', 'FEATURE_FLAG_NAME_2']; +var treatmentResults = client.getTreatmentsWithConfig('user_id', flagNames); + +// Getting treatments by set +treatmentResults = client.getTreatmentsWithConfigByFlagSet('user_id', 'backend'); + +// Getting treatments for the union of multiple sets +var flagSets = ['backend', 'server_side']; +treatmentResults = client.getTreatmentsWithConfigByFlagSets('user_id', flagSets); + +// treatmentResults will have the following form: +// { +// FEATURE_FLAG_NAME_1: {treatment: 'on', +// config: "{ 'color' : 'red'}}", +// FEATURE_FLAG_NAME_2: {treatment: 'v2', +// config: "{ 'copy' : 'better copy'}}", +// } +``` + + +```javascript +// Getting treatments by feature flag names +const flagNames = ['FEATURE_FLAG_NAME_1', 'FEATURE_FLAG_NAME_2']; +let treatmentResults: SplitIO.TreatmentsWithConfig = client.getTreatmentsWithConfig('user_id', flagNames); + +// Getting treatments by set +treatmentResults = client.getTreatmentsWithConfigByFlagSet('user_id', 'backend'); + +// Getting treatments for the union of multiple sets +const flagSets = ['backend', 'server_side']; +treatmentResults = client.getTreatmentsWithConfigByFlagSets('user_id', flagSets); + +// treatmentResults will have the following form: +// { +// FEATURE_FLAG_NAME_1: {treatment: 'on', +// config: "{ 'color' : 'red'}}", +// FEATURE_FLAG_NAME_2: {treatment: 'v2', +// config: "{ 'copy' : 'better copy'}}", +// } +``` + + + +### Shutdown + +Call the `client.destroy()` method before letting a process using the SDK exit, as this method gracefully shuts down the Split SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. + + + +```javascript +user_client.destroy(); +user_client = null; +``` + + + +After `destroy()` is called and finishes, any subsequent invocations to `getTreatment`/`getTreatments` or manager methods result in `control` or empty list, respectively. + +:::warning[Important!] +A call to the `destroy()` method also destroys the factory object. When creating new client instance, first create a new factory instance. +::: + +## Track + +Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users’ actions and metrics. + +[Learn more](https://help.split.io/hc/en-us/articles/360020585772) about using track events in features. + +In the examples below, you can see that the `.track()` method can take up to five arguments. The proper data type and syntax for each are: + +* **key:** The `key` variable used in the `getTreatment` call and firing this track event. The expected data type is **String**. +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of feature flag. +* **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: + * Contains 63 characters or fewer. + * Starts with a letter or number. + * Contains only letters, numbers, hyphen, underscore, or period. + * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` +* **VALUE:** (Optional) The value to be used in creating the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is **Integer** or **Float**. +* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. + +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event to be sent back to Split's servers on the next event post. The SDK will return `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method has been provided. + +In the case that a bad input has been provided, you can read more about our SDK's expected behavior [here](https://help.split.io/hc/en-us/articles/360020585772-Track-events) + + + + +```javascript +// If you would like to send an event without a value +var queuedPromise = client.track('key', 'TRAFFIC_TYPE', 'EVENT_TYPE'); +// Example +var queuedPromise = client.track('john@doe.com', 'user', 'page_load_time'); + +// If you would like to associate a value with an event +var queuedPromise = client.track('key', 'TRAFFIC_TYPE', 'EVENT_TYPE', eventValue); +// Example +var queuedPromise = client.track('john@doe.com', 'user', 'page_load_time', 83.334); + +// If you would like to associate a value and properties with an event +var properties = { package : 'premium', admin : true, discount : 50 }; +var queued = client.track('john@doe.com', 'user', 'page_load_time', 83.334, properties); + +// If you would like to associate only properties with an event +var properties = { package : 'premium', admin : true, discount : 50 }; +var queued = client.track('john@doe.com', 'user', 'page_load_time', null, properties); + +// IF AND ONLY IF you're using Redis storage, the track function returns a promise which will resolve to a boolean +// instead of the boolean itself, indicating if the event was correctly queued or not. +queuedPromise.then(function(queued) { + console.log(queued ? 'Successfully queued event' : 'Failed to queue event'); +}); +``` + + +```javascript +// If you would like to send an event without a value +const queuedPromise: Promise = client.track('key', 'TRAFFIC_TYPE', 'EVENT_TYPE'); +// Example +const queuedPromise: Promise = client.track('john@doe.com', 'user', 'page_load_time'); + +// If you would like to associate a value with an event +const queuedPromise: Promise = client.track('key', 'TRAFFIC_TYPE', 'EVENT_TYPE', eventValue); +// Example +const queuedPromise: Promise = client.track('john@doe.com', 'user', 'page_load_time', 83.334); + +// If you would like to associate a value and properties with an event +const properties = { package : 'premium', admin : true, discount : 50 }; +const queued = client.track('nico@split', 'user', 'page_load_time', 83.334, properties); + +// If you would like to associate only properties with an event +const properties = { package : 'premium', admin : true, discount : 50 }; +const queued = client.track('nico@split', 'user', 'page_load_time', null, properties); + +// IF AND ONLY IF you're using Redis storage, the track function returns a promise which will resolve to a boolean +// instead of the boolean itself, indicating if the event was correctly queued or not. +queuedPromise.then(queued => { + console.log(queued ? 'Successfully queued event' : 'Failed to queue event'); +}); +``` + + + +## Configuration + +The SDK has a number of knobs for configuring performance. Each knob is tuned to a reasonable default. However, you can override the value while instantiating the SDK. The parameters available for configuration are shown below. + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| core.labelsEnabled | Disable labels from being sent to the Split backend. Labels may contain sensitive information. | true | +| core.IPAddressesEnabled | Disable machine IP and Hostname from being sent to Split backend. IP and Hostname may contain sensitive information. | true | +| startup.readyTimeout | Maximum amount of time in seconds to wait before notifying a timeout. Zero means no timeout, so no `SDK_READY_TIMED_OUT` event is fired. | 15 | +| startup.requestTimeoutBeforeReady | Time to wait for a request before the SDK is ready. If this time expires, Node.js SDK tries again `retriesOnFailureBeforeReady` times before notifying its failure to be `ready`. Zero means no timeout. | 15 | +| startup.retriesOnFailureBeforeReady | Number of quick retries we do while starting up the SDK. | 1 | +| scheduler.featuresRefreshRate | The SDK polls Split servers for changes to feature rollout plans. This parameter controls this polling period in seconds. | 60 | +| scheduler.segmentsRefreshRate | The SDK polls Split servers for changes to segment definitions. This parameter controls this polling period in seconds. | 60 | +| scheduler.impressionsRefreshRate | The SDK sends information on who got what treatment at what time back to Split servers to power analytics. This parameter controls how often this data is sent to Split servers. The parameter should be in seconds. | 300 | +| scheduler.impressionsQueueSize | The max amount of impressions we queue. If the queue is full, the SDK flushes the impressions and resets the timer. | 30000 | +| scheduler.eventsPushRate | The SDK sends tracked events to Split servers. This setting controls that flushing rate in seconds. | 60 | +| scheduler.eventsQueueSize | The max amount of events we queue. If the queue is full, the SDK flushes the events and resets the timer. | 500 | +| scheduler.telemetryRefreshRate | The SDK caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers (in seconds). | 3600 seconds (1 hour) | +| sync.splitFilters | Filter specific feature flags to be synced and evaluated by the SDK. This is formed by a type string property and a list of string values for the given criteria. Using the types 'bySet' (recommended, flag sets are available in all tiers) or 'byName', pass an array of strings defining the query. If empty or unset, all feature flags are downloaded by the SDK. | [] | +| sync.impressionsMode | This configuration defines how impressions (decisioning events) are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Split; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Split and only minimum viable data to support usage stats is tracked, so never use this mode if you are experimenting with instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions' network and storage load. In DEBUG mode, ALL impressions are queued and sent to Split; this is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. Keep in mind that both the OPTIMIZED and DEBUG modes utilize an internal cache which uses heap memory incrementally up to a maximum limit ___without a memory leak___. | OPTIMIZED | +| sync.enabled | Controls the SDK continuous synchronization flags. When `true`, a running SDK processes rollout plan updates performed in the Split user interface (default). When `false`, it fetches all data upon init, which ensures a consistent experience during a user session and optimizes resources when these updates are not consumed by the app. | true | +| sync.requestOptions.agent | A custom Node.js HTTP(S) Agent used to perform the requests to the Split servers. See [Proxy](#proxy) for details. | undefined | +| sync.requestOptions.getHeaderOverrides | A callback function that can be used to override the Authentication header or append new headers to the SDK's HTTP(S) requests. | undefined | +| storage.type | Storage type to be used by the SDK. Possible values are `MEMORY`, and `REDIS`. | `MEMORY` | +| storage.options | Options to be passed to the storage instance. Only usable with `REDIS` type storage for now. See [Redis configuration](#redis-configuration) for details. | {}
No default options | +| storage.prefix | An optional prefix for your data, to avoid collisions. | `SPLITIO` | +| mode | The SDK mode. Possible values are standalone and consumer. | standalone | +| debug | Boolean flag or log level string ('ERROR', 'WARN', 'INFO', or 'DEBUG') for activating SDK logs. | false | +| streamingEnabled | Boolean flag to enable the streaming service as default synchronization mechanism. In the event of an issue with streaming, the SDK will fallback to the polling mechanism. If false, the SDK will poll for changes as usual without attempting to use streaming. | true | + +To set each of the parameters defined above, use the following syntax. + + + +```javascript +var factory = SplitFactory({ + core: { + authorizationKey: 'YOUR_API_KEY', + labelsEnabled: true + }, + scheduler: { + featuresRefreshRate: 5, // 5 sec + segmentsRefreshRate: 60, // 60 sec + impressionsRefreshRate: 300, // 300 sec + impressionsQueueSize: 30000, // 30000 items + eventsPushRate: 60, // 60 sec + eventsQueueSize: 500, // 500 items + telemetryRefreshRate: 3600 // 1 hour + }, + startup: { + requestTimeoutBeforeReady: 1.5, // 1500 ms + retriesOnFailureBeforeReady: 3, // 3 times + readyTimeout: 5 // 5 sec + }, + sync: { + splitFilters: [{ + type: 'bySet', + values: ['backend'] + }], + impressionsMode: 'DEBUG', + requestOptions: { + getHeaderOverrides() { + return { + 'custom-header': 'custom-value' + }; + } + } + }, + storage: { + type: 'REDIS', + options: {}, + prefix: 'MYPREFIX' + }, + mode: 'standalone', + debug: false +}); +``` + + +```javascript +const factory: SplitIO.ISDK = SplitFactory({ + core: { + authorizationKey: 'YOUR_API_KEY', + labelsEnabled: true + }, + scheduler: { + featuresRefreshRate: 5, // 5 sec + segmentsRefreshRate: 60, // 60 sec + impressionsRefreshRate: 300, // 300 sec + impressionsQueueSize: 30000, // 30000 items + eventsPushRate: 60, // 60 sec + eventsQueueSize: 500, // 500 items + telemetryRefreshRate: 3600 // 1 hour + }, + startup: { + requestTimeoutBeforeReady: 1.5, // 1500 ms + retriesOnFailureBeforeReady: 3, // 3 times + readyTimeout: 5 // 5 sec + }, + sync: { + splitFilters: [{ + type: 'bySet', + values: ['backend'] + }], + impressionsMode: 'DEBUG', + requestOptions: { + getHeaderOverrides() { + return { + 'custom-header': 'custom-value' + }; + } + } + }, + storage: { + type: 'REDIS', + options: {}, + prefix: 'MYPREFIX' + }, + mode: 'standalone', + debug: false +}); +``` + + + +## State sharing with Redis + +**Configuring this Redis integration section is optional for most setups. Read below to determine if it might be useful for your project.** + +By default, the Split client stores the state it needs to compute treatments (rollout plans, segments, and so on) in memory. As a result, it is easy to get set up with Split: simply instantiate a client and start using it. + +This simplicity hides one important detail that is worth exploring. Because each Split client downloads and stores state separately, a change in a feature flag is picked up by every client on its own schedule. Thus, if a customer issues back-to-back requests that are served by two different machines behind a load balancer, the customer can see different treatments for the same feature flag because one Split client may not have picked up the latest change. This drift in clients is natural and usually ignorable as long as each client sets an aggressive value for `FeaturesRefreshRate` and `SegmentsRefreshRate`. You can learn more about setting these rates in the [Configuration section](#configuration) below. + +However, if your application requires a total guarantee that Split clients across your entire infrastructure pick up a change in a feature flag at the exact same time or you need an async data store, then the only way to ensure that is to externalize the state of the Split client in a data store hosted on your infrastructure. + +We currently support Redis for this external data store. + +To use the Node.js SDK with Redis, set up the Split Synchronizer and instantiate the SDK in consumer mode. + +#### Split Synchronizer + +Follow the steps in our [Split Synchronizer](https://help.split.io/hc/en-us/articles/360019686092) documents to get everything set to sync data to your Redis cache. After you do that, come back to set up the SDK in consumer mode! + +#### Consumer mode + +In consumer mode, a client can be embedded in your application code and respond to calls to `getTreatment` by retrieving state from the data store (Redis in this case). + +Here is how to configure and get treatments for a Split client in consumer mode. + + + +```javascript +var SplitFactory = require('@splitsoftware/splitio').SplitFactory; + +var config = { + mode: 'consumer', // changing the mode to consumer here + core: { + authorizationKey: '' + }, + // defining the location of the Redis cache that the SDK should talk to + storage: { + type: 'REDIS', + options: { + url: 'redis://:/0' + }, + prefix: 'nodejs' // Optional prefix to prevent any kind of + // data collision between SDK versions. + } +}; + +var factory = SplitFactory(config); +var client = factory.client(); + +// Redis in Node.js is async, so the operation is executed asynchronously. +// You have as 2 different syntaxes to getTreatments: + +// one is the async/await syntax +var treatment = await client.getTreatment('user_id', 'my-feature-flag-coming-from-redis'); + +// or just using the returned promise +client.getTreatment('user_id', 'my-feature-flag-coming-from-redis') + .then(treatment => { + // do something with the treatment + }); + +// You can optionally listen at the following events in consumer mode: + +client.once(client.Event.SDK_READY, function () { + // This callback will be called once the connection with Redis is stablished. + // There is no need to wait for this event before using the SDK, since the promise will resolve once it could get + // the data (including Redis connection) and perform the operation or reject in case of error, including timeouts. +}); + +client.once(client.Event.SDK_READY_TIMED_OUT, function () { + // This callback will be called after the seconds set at the `startup.readyTimeout` config parameter, + // if and only if the SDK_READY event was not emitted for that time. +}); +``` + + +```javascript +import { SplitFactory } from '@splitsoftware/splitio'; + +const config: SplitIO.INodeAsyncSettings = { + mode: 'consumer', // changing the mode to consumer here + core: { + authorizationKey: '' + }, + // defining the location of the Redis cache that the SDK should talk to + storage: { + type: 'REDIS', + options: { + url: 'redis://:/0' + }, + prefix: 'nodejs' // Optional prefix to prevent any kind of + // data collision between SDK versions. + } +}; + +const factory: SplitIO.IAsyncSDK = SplitFactory(config); +const client: SplitIO.IAsyncClient = factory.client(); + +// Redis in Node.js is async. This means we run the evaluation in a async way. +// You have 2 different syntaxes to interact with getTreatment results: + +// One, by just using the returned promise +client.getTreatment('user_id', 'my-feature-flag-coming-from-redis') + .then(treatment => { + // do something with the treatment + }); + +// Or you can use the async/await syntax +const treatment = await client.getTreatment('user_id', 'my-feature-flag-coming-from-redis'); +// do something with the treatment + +// NOTE: async/await is supported for all targets since TypeScript 2.1. +// See https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#downlevel-async-functions for details. +``` + + + +#### Redis configuration + +The SDK in consumer mode connects to Redis to function, using URL `redis://localhost:6379/0` by default. You can override this URL and other Redis connection parameters with the SDK `storage.options` configuration object. The available parameters are shown below. + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| host | Hostname where the Redis instance is. | `localhost` | +| port | HTTP port to be used in the connection. | 6379 | +| db | Numeric database to be used. | 0 | +| pass | Redis DB password. Don't define if no password is used. | undefined | +| url | Redis URL. If set, `host`, `port`, `db` and `pass` params will be ignored. Example: `redis://:authpassword@127.0.0.1:6379/0` | undefined | +| tls | TLS configuration object. See [ioredis TLS Options](https://www.npmjs.com/package/ioredis#tls-options) for details. | undefined | +| connectionTimeout | The milliseconds before a timeout occurs during the initial connection to the Redis server. | 10000 | +| operationTimeout | The milliseconds before Redis commands are timeout by the SDK. Method calls that involve Redis commands, like `client.getTreatment` or `client.track` calls, are resolved when the commands success or timeout. | 5000 | + +## Localhost mode + +For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the Split SDK can be started in **localhost** mode (aka off-the-grid mode). In this mode, the SDK neither polls nor updates Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the feature flags. + +To use the SDK in localhost mode, set the `authorizationKey` config property to "localhost", as shown in the example below: + + + +```javascript +var factory = SplitFactory({ + core: { + authorizationKey: 'localhost' + }, + features: path.join(__dirname, '.split'), + scheduler: { + offlineRefreshRate: 15 // 15 sec + } +}); + +var client = factory.client(); + +client.on(client.Event.SDK_READY, function() { + // The following code will be evaluated once the engine finalizes + // the initialization + var t1 = client.getTreatment('user_id', 'reporting_v2'); + var t2 = client.getTreatment('user_id', 'billing_updates'); + var t3 = client.getTreatment('user_id', 'navigation_bar_changes'); +}); +``` + + +```javascript +const factory: SplitIO.ISDK = SplitFactory({ + core: { + authorizationKey: 'localhost' + }, + features: path.join(__dirname, '.split'), + scheduler: { + offlineRefreshRate: 15 // 15 sec + } +}); + +const client: SplitIO.IClient = factory.client(); + +client.on(client.Event.SDK_READY, function() { + // The following code will be evaluated once the engine finalizes + // the initialization + const t1: SplitIO.Treatment = client.getTreatment('user_id', 'reporting_v2'); + const t2: SplitIO.Treatment = client.getTreatment('user_id', 'billing_updates'); + const t3: SplitIO.Treatment = client.getTreatment('user_id', 'navigation_bar_changes'); +}); +``` + + + +In this mode, the SDK loads a mapping of feature flag name to treatment from a file at `$HOME/.split`. For a given feature flag, the treatment specified in the file is returned for every customer. Should you want to use another file, you just need to set the `features` key in the configuration object passed at instantiation time, to the full path of the desired file. + +`getTreatment` calls for a feature flag only return the one treatment that you defined in the file. You can then change the treatment as necessary for your testing in the file. Any feature flag that is not provided in the `features` map returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment) if the SDK is asked to evaluate them. + +Here is a sample `.split` file. The format of this file is two columns separated by a whitespace. The left column is the feature flag name, and the right column is the treatment name. + + + +```bash +## this is a comment + +reporting_v2 on ## sdk.getTreatment(*, reporting_v2) will return 'on' + +double_writes_to_cassandra off + +new-navigation v3 +``` + + + +**Since version 10.7.0**, our SDK supports a new type of localhost feature flag definition file, using the YAML format. This new format allows the user to map different keys to different treatments within a single feature flag, and also add configurations to them for a given treatment. The new format is a list of single-key maps (one per mapping split-keys-config), defined as follows: + + + +```yaml +## - feature_name: +## treatment: "treatment_applied_to_this_entry" +## keys: ["single_key_or_list", "other_key_same_treatment"] +## config: "{\"desc\" : \"this applies only to ON treatment\"}" +# +## Note that the "treatment" key is mandatory, but both "keys" and "config" are optional. + +- my_feature: + treatment: "on" + keys: "mock_user_id" + config: "{\"desc\" : \"this applies only to ON treatment\"}" +- some_other_feature: + treatment: "off" +- my_feature: + treatment: "off" +``` + + + +In the example above, we have 3 entries: + * The first one defines that for feature flag `my_feature`, the key `mock_user_id` will return the treatment `on` and the `on` treatment will be tied to the configuration `{"desc" : "this applies only to ON treatment"}`. + * The second entry defines that the feature flag `some_other_feature` will always return the `off` treatment and no configuration. + * The third entry defines that `my_feature` will always return `off` for all keys that don't match another entry (in this case, any key other than `mock_user_id`). + +In addition, there are some extra configuration parameters that can be used when instantiating the SDK in localhost mode. + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| scheduler.offlineRefreshRate | The refresh interval for the mocked feature flags treatments. | 15 | +| features | The path to the file with the mocked feature flag data. | `$HOME/.split` | + +## Manager + +Use the Split Manager to get a list of feature flags available to the Split client. + +To instantiate a Manager in your code base, use the same factory that you used for your client. + + + +```javascript +var factory = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY' + } +}); + +var manager = factory.manager(); + +manager.once(manager.Event.SDK_READY, function() { + // Once it's ready, use the manager +}); +``` + + +```javascript +const factory: SplitIO.ISDK = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY' + } +}); + +const manager: SplitIO.IManager = factory.manager(); + +manager.once(manager.Event.SDK_READY, function() { + // Once it's ready, use the manager +}); +``` + + + +The Manager instance has the following methods available. + + + +```javascript +/** + * Returns the feature flag registered with the SDK of this name. + * + * @return SplitView or null. + */ +var splitView = manager.split('FEATURE_FLAG_NAME'); + +/** + * Retrieves the feature flags that are currently registered with the + * SDK. + * + * returns a List of SplitViews. + */ +var splitViewsList = manager.splits(); + +/** + * Returns the names of feature flags registered with the SDK. + * + * @return a List of Strings of the feature flag names. + */ +var splitNamesList = manager.names(); +``` + + +```javascript +/** + * Returns the feature flag registered with the SDK of this name. + * + * @return SplitView or null. + */ +var splitView: SplitIO.SplitView = manager.split('FEATURE_FLAG_NAME'); + +/** + * Retrieves the feature flags that are currently registered with the + * SDK. + * + * returns a List of SplitViews. + */ +var splitViewsList: SplitIO.SplitViews = manager.splits(); + +/** + * Returns the names of feature flags registered with the SDK. + * + * @return a List of Strings of the features' names. + */ +var splitNamesList: SplitIO.SplitNames = manager.names(); +``` + + + +The `SplitView` object referenced above has the following structure: + +```typescript title="TypeScript" +type SplitView = { + name: string, + trafficType: string, + killed: boolean, + treatments: Array, + changeNumber: number, + configs: { + [treatmentName: string]: string + }, + defaultTreatment: string, + sets: Array, + impressionsDisabled: boolean +} +``` + +## Listener + +Split SDKs send impression data back to Split servers periodically when evaluating feature flags. To send this information to a location of your choice, define and attach an *impression listener*. Use the SDK's `impressionListener` parameter, where you can add an implementation of `ImpressionListener`. This implementation **must** define the `logImpression` method. It receives data in the following schema. + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| impression | Object / SplitIO.Impression | Impression object that has the feature flag, key, treatment, label, etc. | +| attributes | Object / SplitIO.Attributes | A map of attributes passed to `getTreatment`/`getTreatments` (if any). | +| ip | String | The IP address of the machine where the SDK is running. | +| hostname | String | The hostname of the OS where the SDK is running. | +| sdkLanguageVersion | String | The version of the SDK. In this case the language is `nodejs` plus the version currently running. | + +## Implement a custom impression listener + +Here is an example of how to implement a custom impression listener. + + + +```javascript +function logImpression(impressionData) { + // do something with the impression data. +} + +var factory = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY' + }, + impressionListener: { + logImpression: logImpression + } +}); +``` + + +```javascript +class MyImprListener implements SplitIO.IImpressionListener { + logImpression(impressionData: SplitIO.ImpressionData) { + // do something with impressionData + } +} + +const factory: SplitIO.ISDK = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY' + }, + impressionListener: { + logImpression: new MyImprListener() + } +}); +``` + + + +An impression listener is called asynchronously from the corresponding evaluation, but is almost immediate. + +The SDK does not fail if there is an exception in the listener, but be careful to avoid blocking the call stack. + +## Logging + +To enable SDK logging in your Node.js app, set the `SPLITIO_DEBUG` environment variable as follows. + + + +```bash +## Acceptable values are 'DEBUG', 'INFO', 'WARN', 'ERROR' and 'NONE' +## Other acceptable values are 'on', 'enable' and 'enabled', which are equivalent to 'DEBUG' log level +SPLITIO_DEBUG='on' node app.js +``` + + + +Since v9.2.0 of the SDK, you can enable logging via SDK settings and programmatically by calling the Logger API. + + + +```javascript +var SplitFactory = require('@splitsoftware/splitio').SplitFactory; + +var factory = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY' + }, + debug: true // Debug boolean option can be passed on settings. +}); + +// Or you can use the Logger API which two methods, enable and disable. +// Calling this methods will have an immediate effect. +factory.Logger.enable(); +factory.Logger.disable(); + +// You can also set the log level programatically after v10.4.0 +// Acceptable values are: 'DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE'. +// 'DEBUG' is equivalent to `enable` method. +// 'NONE' is equivalent to `disable` method. +factory.Logger.setLogLevel('WARN'); +``` + + +```javascript +import { SplitFactory } from '@splitsoftware/splitio'; + +const factory: SplitIO.ISDK = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY' + }, + debug: true // Debug boolean option can be passed on settings +}); + +// Or you can use the Logger API which two methods, enable and disable. +// Calling this methods will have an immediate effect. +factory.Logger.enable(); +factory.Logger.disable(); + +// You can also set the log level programatically after v10.4.0 +// Acceptable values are: 'DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE'. +// 'DEBUG' is equivalent to `enable` method. +// 'NONE' is equivalent to `disable` method. +factory.Logger.setLogLevel('WARN'); +``` + + + +Example output is shown below. + +

f517ed3-Logs_output.png

+ +:::info[Note] +For more information on using the logging framework in SDK versions prior to 9.2, visit [https://github.com/visionmedia/debug](https://github.com/visionmedia/debug). +::: + +## Proxy + +If you need to use a network proxy, you can provide a custom [Node.js HTTPS Agent](https://nodejs.org/api/https.html#class-httpsagent) by setting the `sync.requestOptions.agent` configuration variable. The SDK will use this agent to perform requests to Split servers. + + + +```javascript +// Install with `npm install https-proxy-agent` +const { HttpsProxyAgent } = require('https-proxy-agent'); +const { SplitFactory } = require('@splitsoftware/splitio'); + +const proxyAgent = new HttpsProxyAgent(process.env.HTTPS_PROXY || 'http://10.10.1.10:1080'); + +const factory = SplitFactory({ + core: { + authorizationKey: 'YOUR_SDK_KEY' + }, + sync: { + requestOptions: { + agent: proxyAgent + } + } +}) +``` + + + +## Advanced use cases + +This section describes advanced use cases and features provided by the SDK. + +### Working with both sync and async storage + +You can write code that works with all type of SDK storage. For example, you might have an application that you want to run on both `REDIS` and `MEMORY` storage types. To accommodate this, check if the treatments are [thenable objects](https://promisesaplus.com/#terminology) to decide when to execute the code that depends on the feature flag. + +See example below. + + + +```javascript +var treatment = client.getTreatment('key', 'FEATURE_FLAG_NAME'); + +if (thenable(treatment)) { + // We have a promise so we will use the treatment in the callback, which will receive the treatment string. + treatment.then(useTreatment); +} else { + // We have the actual string. + useTreatment(treatment); +} + +function useTreatment(splitTreatment) { + if (splitTreatment == 'on') { + // insert code here to show on treatment + } else if (splitTreatment == 'off') { + // insert code here to show off treatment + } else { + // insert your control treatment code here + } +} + +function thenable(val) { + // By definition, “thenable” is an object or function that defines a then method. + return val !== undefined && typeof val.then === 'function'; +} +``` + + +```javascript +const treatment: (SplitIO.Treatment | SplitIO.AsyncTreatment) = + client.getTreatment('key', 'FEATURE_FLAG_NAME'); + +if (thenable(treatment)) { + // We have a promise so we will use the treatment in the cb, + // which will receive the treatment string. + treatment.then(useTreatment); +} else { + // We have the actual string. + useTreatment(treatment); +} + +function useTreatment(splitTreatment) { + if (splitTreatment == 'on') { + // insert code here to show on treatment + } else if (splitTreatment == 'off') { + // insert code here to show off treatment + } else { + // insert your control treatment code here + } +} + +function thenable(val) { + // By definition, “thenable” is an object or function that defines a then method. + return val !== undefined && typeof val.then === 'function'; +} +``` + + diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/php-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/php-sdk.md new file mode 100644 index 00000000000..da6bf635a33 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/php-sdk.md @@ -0,0 +1,672 @@ +--- +title: PHP SDK +sidebar_label: PHP SDK +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +This guide provides detailed information about our PHP SDK. All of our SDKs are open source. Go to our [PHP SDK GitHub repository](https://github.com/splitio/php-client) to learn more. + +## Language support + +The PHP SDK supports PHP language version 7.3 and later. + +## Initialization + +### SDK architecture + +The PHP SDK is architected differently from our other SDKs. This is because of the **share nothing** nature of PHP, which means that you need to leverage a remote data store that your processes can share to ensure that your customers are served consistent treatments from our SDK. The SDK has three components. + +#### SDK client + +The SDK client is embedded within your PHP app. It decides which treatment to show to a customer for a particular feature flag. + +#### Split Synchronizer + +The Split Synchronizer service fetches data from the Split servers so it can evaluate what treatment to show to a customer. This is a background service that can run on one machine on a schedule via your scheduling system. Refer to the [Split Synchronizer documentation](https://help.split.io/hc/en-us/articles/360019686092) for more information. + +#### Cache + +The Synchronizer Service stores its fetched data in the cache component. The SDK comes pre-packaged with a Redis Cache Adapter. It is critical that Redis is configured **to never evict**. + +Now that you have a sense of how this SDK is structured, follow the steps below to set up the SDK in your code base: + +### 1. Import the SDK into your project + +```php title="PHP 7.3+" +composer require splitsoftware/split-sdk-php:7.3.0 +``` + +The public release is available at [packagist.org/packages/splitsoftware/split-sdk-php](https://packagist.org/packages/splitsoftware/split-sdk-php). + +:::warning[If using Synchronizer with Redis - Synchronizer 2.x required after SDK Version 3.x] +Since version 2.0.0 of the split-synchronizer, we use a more efficient scheme to store impressions in Redis. This approach is faster and easier on your Redis instances, since it yields better throughput of impressions to the backend. If you use this SDK with the Synchronizer in Redis or Proxy mode, you need the newest versions of our Split Synchronizer. It is recommended that once you're using SDK versions compatible with Split-Sync 2.0 on all your applications pointing to the redis instance maintained by the Split-Sync, you disable backwards compatibility. This is as easy as changing a parameter to `true` on the JSON config or an environment variable to `on` if you're using the docker image. +::: + +### 2. Set up the synchronizer service + +When the composer is done, follow the steps in our [Split Synchronizer](https://help.split.io/hc/en-us/articles/360019686092) documents to get everything set to sync data to your Redis cache. After you do that, come back to set up the SDK in consumer mode! + +### 3. Instantiate the SDK and create a new split client + +We recommend instantiating the Split factory once as a singleton and reusing it throughout your application. + +Use the code snippet below to instantiate the client in your code base. You need to provide your Redis details and your SDK API key. + +Configure the SDK with the SDK API key for the Split environment that you would like to access. The SDK key is available in the Split user interface, on your Admin settings page, API keys section. Select a server-side SDK API key. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. + +Do all of this as a part of the startup sequence of your application. + +```php title="PHP" + 'tcp', + 'host' => REDIS_HOST, + 'port' => REDIS_PORT, + 'timeout' => 881 + ]; + +$options = ['prefix' => '']; + +$sdkConfig = array( + 'cache' => array('adapter' => 'predis', + 'parameters' => $parameters, + 'options' => $options + ) +); + +$splitFactory = \SplitIO\Sdk::factory('YOUR_SDK_KEY', $sdkConfig); +$splitClient = $splitFactory->client(); +``` + +## Using the SDK + +### Basic use + +After you instantiate the SDK client, use the `getTreatment` method of the SDK client to decide what version of your feature flags your customers are served. The method requires the `FEATURE_FLAG_NAME` attribute that you want to ask for a treatment and a unique `key` attribute that corresponds to the end user that you want to serve the feature flag to. + +From there, you need to use an if-else-if block as shown below and insert the code for the different treatments that you defined in the Split user interface. Remember the final else branch in your code to handle the client returning [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). + +```php title="PHP" +getTreatment('key','FEATURE_FLAG_NAME'); + +if ($treatment === 'on') { + // insert code here to show on treatment +} elseif ($treatment === 'off') { + // insert code here to show off treatment +} else { + // insert your control treatment code here +} +``` + +### Attribute syntax + +To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK's `getTreatment` method needs to pass an attribute map at runtime. + +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in the Split user interface to decide whether to show the `on` or `off` treatment to this account. + +The `getTreatment` method supports five types of attributes: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: + +* **Strings:** Use type String. +* **Numbers:** Use type Integer. +* **Dates:** Express the value in `seconds since epoch`. Use a timestamp represented by an Integer. +* **Booleans:** Use type Boolean. +* **Sets:** Use type Array. + +```php title="PHP" +getTimestamp(); +$attributes["deal_size"] = 10000; +$attributes["paying_customer"] = True; +$attributes["permissions"] = array("gold","silver","platinum"); + +$treatment = $splitClient->getTreatment('key', 'FEATURE_FLAG_NAME', $attributes); + +if ($treatment === 'on') { + // insert code here to show on experience +} elseif ($treatment === 'off') { + // insert code here to show off experience +} else { + // insert your control treatment code here to show no reporting +} +``` + +### Multiple evaluations at once + +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` from the Split client to do this. +* `getTreatments`: Pass a list of the feature flag names you want treatments for. +* `getTreatmentsByFlagSet`: Evaluate all flags that are part of the provided set name and are cached on the SDK instance. +* `getTreatmentsByFlagSets`: Evaluate all flags that are part of the provided set names and are cached on the SDK instance. + + + +```php +$treatments = $splitClient->getTreatments('key', ['FEATURE_FLAG_NAME_1', 'FEATURE_FLAG_NAME_2']); + +echo json_encode($treatments); +``` + + +```php +$treatments = $splitClient->getTreatmentsByFlagSet('key', 'backend'); + +echo json_encode($treatments); +``` + + +```php +$treatments = $splitClient->getTreatmentsByFlagSets('key', ['backend', 'server_side']); + +echo json_encode($treatments); +``` + + + +You can also use the [Split Manager](#manager) to get all of your treatments at once. + +### Get Treatments with Configurations + +To [leverage dynamic configurations with your treatments](https://help.split.io/hc/en-us/articles/360026943552), you should use the `getTreatmentWithConfig` method. This method returns an object containing the treatment and associated configuration. + +The config element is a stringified version of the configuration JSON defined in the Split user interface. If there is no configuration defined for a treatment, the SDK returns `null` for the config parameter. + +This method takes the exact same set of arguments as the standard `getTreatment` method. See below for examples on proper usage: + +```php title="PHP" +$result = $splitClient->getTreatmentWithConfig("KEY", "FEATURE_FLAG_NAME", attributes); +$config = json_decode($result["config"], true); +$treatment = $result["treatment"]; +``` + +If you need to get multiple evaluations at once, you can also use the `getTreatmentsWithConfig` methods. These methods take the exact same arguments as the [getTreatments](#multiple-evaluations-at-once) methods but return a mapping of feature flag names to SplitResult instead of strings. See example usage below: + + + +```php +$TreatmentResults = $splitClient->getTreatmentsWithConfig('KEY', array('FEATURE_FLAG_NAME_1' 'FEATURE_FLAG_NAME_2'), attributes); +// TreatmentResults will have the following form: +// { +// FEATURE_FLAG_NAME_1: {treatment: 'on', +// config: "{ 'color' : 'red'}}", +// FEATURE_FLAG_NAME_2: {treatment: 'v2', +// config: "{ 'copy' : 'better copy'}}", +// } +``` + + +```php +$treatments = $splitClient->getTreatmentsWithConfigByFlagSet('KEY', 'backend') +// $treatments will have the following form: +// { +// FEATURE_FLAG_NAME_1: {treatment: 'on', +// config: "{ 'color' : 'red'}}", +// FEATURE_FLAG_NAME_2: {treatment: 'v2', +// config: "{ 'copy' : 'better copy'}}", +// } +``` + + +```php +$treatments = $splitClient->getTreatmentsWithConfigByFlagSets('KEY', ['backend', 'server_side']) +// $treatments will have the following form: +// { +// FEATURE_FLAG_NAME_1: {treatment: 'on', +// config: "{ 'color' : 'red'}}", +// FEATURE_FLAG_NAME_2: {treatment: 'v2', +// config: "{ 'copy' : 'better copy'}}", +// } +``` + + + +### Shutdown + +Due to the nature of PHP and the way HTTP requests are handled, the client is instantiated on every request and automatically destroyed when the request lifecycle comes to an end. The data is stored in Redis, which is populated by an external synchronization tool, so it does not make sense to provide a shutdown method within the PHP client. If desired, you can manually start or stop the Split Synchronizer and flush the Redis database. + +## Track + +Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users’ actions and metrics. + +Refer to the [Events](https://help.split.io/hc/en-us/articles/360020585772) documentation for more information about using track events in feature flags. + +In the examples below you can see that the `.track()` method can take up to five arguments. The proper data type and syntax for each are: + +* **key:** The `key` variable used in the `getTreatment` call and firing this track event. The expected data type is **String**. +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: + * Contains 63 characters or fewer. + * Starts with a letter or number. + * Contains only letters, numbers, hyphen, underscore, or period. + * This is the regular expression we use to validate the value:
`[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` +* **VALUE:** (Optional) The value to be used in creating the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is **Integer** or **Float**. +* **PROPERTIES:** (Optional) An Map of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. + +:::warning[Redis Support] +If you are using our SDK with Redis, you need Split Synchronizer v2.3.0 version at least in order to support *properties* in the `track` method. +::: + +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event to be sent back to Split's servers on the next event post. The SDK will return `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method has been provided. + +In the case that a bad input has been provided, you can read more about our SDK's expected behavior in the [Events documentation](https://help.split.io/hc/en-us/articles/360020585772-Track-events) + +```php title="PHP 7.3+" +track("KEY", "TRAFFIC_TYPE", "EVENT_TYPE"); +// Example +$trackEvent = $splitClient->track("john@doe.com", "user", "page_load_time"); + +// If you would like to associate a value to an event +$trackEvent = $splitClient->track("KEY", "TRAFFIC_TYPE", "EVENT_TYPE", VALUE); +// Example +$trackEvent = $splitClient->track("john@doe.com", "user", "page_load_time", 83.334); + +// If you would like to associate just properties to an event +$trackEvent = $splitClient->track("KEY", "TRAFFIC_TYPE", "EVENT_TYPE", null, {PROPERTIES}); + +// If you would like to associate a value and properties to an event +$trackEvent = $splitClient->track("KEY", "TRAFFIC_TYPE", "EVENT_TYPE", VALUE, {PROPERTIES}); +// Example +$properties = array( + "package" => "premium", + "admin" => true, + "discount" => 50 +); +$trackEvent = $splitClient->track("KEY", "TRAFFIC_TYPE", "EVENT_TYPE", 83.334, $properties); +``` + +## Configuration + +With the SDK architecture, there is a set of options that you can configure to get everything connected and working as expected. + +| **Option** | **Description** | +| --- | --- | +| labelsEnabled | Enable a descriptive label for each generated impression. Refer to the [Impressions data](#impressions-data) section below. | +| ipAddress | Manually set the IP address of your server to attach as metadata when impressions are sent. | +| log | Configure the log adapter and level. Refer to the [Logging](#logging) section. | +| cache | Configure the Redis cache adapter. Refer to the [Redis cache](#redis-cache) section. | +| impressionListener | Instance of an impression listener to send impression data to a custom location. | +| IPAddressesEnabled | Flag to disable IP addresses and host name from being sent to the Split backend. | + +```php title="PHP" + false, + 'ipAddress' => '15.2.34.123', + 'log' => array(...), + 'cache' => array(...), +); + +$splitFactory = \SplitIO\Sdk::factory('YOUR_SDK_KEY', $sdkConfig); +$splitClient = $splitFactory->client(); +``` + +### Impressions data + +By default, our SDK sends small amounts of information to our backend about why your customer saw a given treatment from your app. An example would be that a user saw the `on` treatment because they were `in segment all`. This information can be useful for experiments and debugging. However, if you prefer to not send this information to our servers, you can turn it off by setting the SDK configuration option `labelsEnabled`. + +### Redis cache + +The Synchronizer Service stores its fetched data in the cache component. The SDK requires `predis` as the Redis cache adapter. For more information about `predis`, refer to the [Predis project](https://github.com/nrk/predis). + +```php title="PHP" + 'tcp', + 'host' => REDIS_HOST, + 'port' => REDIS_PORT, + 'timeout' => 881 + ]; + +$options = ['prefix' => '']; + +$sdkConfig = array( + 'cache' => array('adapter' => 'predis', + 'parameters' => $parameters, + 'options' => $options + ) +); + +$splitFactory = \SplitIO\Sdk::factory('YOUR_SDK_KEY', $sdkConfig); +$splitClient = $splitFactory->client(); +``` + +#### Redis Sentinel + +The SDK also supports Redis with Sentinel v2 replication. The client can be configured to operate with a single master and multiple slaves to provide high availability. The current version of Sentinel is 2. A stable release of Sentinel has been shipped since Redis 2.8. For further information about Sentinel, refer to the [Sentinel documentation](https://redis.io/topics/sentinel). + +Use the following configuration for Redis in Sentinel mode. + +| **Variable** | **Type** | **Description** | +| --- | --- | --- | +| distributedStrategy | string | The strategy to be used. For Sentinel mode, the strategy is `sentinel`. | +| sentinels | array | The list of sentinels for replication service. | +| service | string | The name of master service. | + +```php title="PHP" + 'sentinel', + 'service' => 'SERVICE_MASTER_NAME', + 'prefix' => '' +); + +$sdkConfig = array( + 'cache' => array('adapter' => 'predis', + 'sentinels' => $sentinels, + 'options' => $options + ) +); + +$splitFactory = \SplitIO\Sdk::factory('YOUR_SDK_KEY', $sdkConfig); +$splitClient = $splitFactory->client(); +``` + +#### Redis cluster support + +The SDK supports Redis with Cluster. Note that a stable release of Cluster has shipped since Redis 3.0. For further information about Redis Cluster, refer to the [Cluster documentation](https://redis.io/topics/cluster-spec). + +Use the following configuration for Redis in Cluster mode. + +| **Variable** | **Type** | **Description** | +| --- | --- | --- | +| distributedStrategy | string | The strategy to be used. For Cluster mode, the strategy is `cluster`. | +| clusterNodes | array | The list of cluster nodes. | +| keyHashTag | string | Custom hashtag to be used. Default value is `{SPLITIO}`. | +| keyHashTags | array | List of custom hashtags from which the SDK randomly picks one to use on the generated instance. If this is set, the tag on `keyHashTag` config is ignored. | + +```php title="PHP" + 'cluster', + 'prefix' => '' +); + +$sdkConfig = array( + 'cache' => array('adapter' => 'predis', + 'clusterNodes' => $clusterNodes, + 'options' => $options + ) +); + +$splitFactory = \SplitIO\Sdk::factory('YOUR_SDK_KEY', $sdkConfig); +$splitClient = $splitFactory->client(); +``` + +:::info[Redis Cluster] +The PHP SDK performs multi-key operations in certain methods such as `mget` (to return values of all specified keys) or `keys` (to return all the keys that matches a particular pattern) to avoid multiple calls to Redis. Redis Cluster does not allow these operations unless you use hashtags. Hashtags ensure that multiple keys are allocated in the same hash slot. The SDK allows you to use a custom key hashtag for storing keys. If this option is missing, it uses a default hashtag of `{SPLITIO}` when `cluster` mode is specified in the configuration. Keep in mind that multi-key operations may become unavailable during a resharding of the hash slots, calls to `getTreatments`, or `manager.splitNames()`, causing `featureFlagKeys` to fail. +::: + +#### TLS Support + +The client can leverage TLS/SSL encryption to connect to a secured remote Redis instance by using `tls` scheme and `ssl` options. + +```php title="PHP" + 'tls', + 'ssl' => array(), + 'host' => REDIS_HOST, + 'port' => REDIS_PORT, + ]; + +$options = ['prefix' => '']; + +$sdkConfig = array( + 'cache' => array('adapter' => 'predis', + 'parameters' => $parameters, + 'options' => $options + ) +); + +$splitFactory = \SplitIO\Sdk::factory('YOUR_SDK_KEY', $sdkConfig); +$splitClient = $splitFactory->client(); +``` + +## Localhost mode + +For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the Split SDK can be started in **localhost** mode (aka off-the-grid mode). In this mode, the SDK neither polls nor updates Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the features. + +To use the SDK in localhost mode, replace the SDK Key with "localhost", as shown in the example below: + +```php title="PHP" +client(); +``` + +In this mode, the SDK loads a mapping of feature flag name to treatment from a file at `$HOME/.split`. For a given feature flag, the treatment specified in the file is returned for every customer. + +`getTreatment` calls for a feature flag only return the one treatment that you defined in the file. You can then change the treatment as necessary for your testing in the file. Any feature flag that is not provided in the `featureFlags` map returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment) if the SDK is asked to evaluate them. + +The format of this file is two columns separated by a whitespace. The left column is the feature flag name, and the right column is the treatment name. Here is a sample `.split` file. + +```bash title="Shell" +## this is a comment + +## sdk.getTreatment(*, reporting_v2) will return 'on' +reporting_v2 on + +## sdk.getTreatment(*, double_writes_to_cassandra) will return 'off' +double_writes_to_cassandra off + +## sdk.getTreatment(*, new-navigation) will return 'v3' +new-navigation v3 +``` + +Since version 6.1.0, our SDK supports a new type of localhost feature flag definition file, using the YAML format. This new format allows the user to map different keys to different treatments within a single feature flag, and also adds configurations to them. The new format is a list of single-key maps (one per mapping feature-flag-keys-config), defined as follows: + +```yaml title="YAML" +## - feature_flag_name: +## treatment: "treatment_applied_to_this_entry" +## keys: "single_key_or_list" +## config: "{\"desc\" : \"this applies only to ON treatment\"}" +- my_feature: + treatment: "on" + keys: "key" + config: "{\"desc\" : \"this applies only to ON treatment\"}" +- some_other_feature: + treatment: "off" +- my_feature: + treatment: "off" +``` + +In the example above, we have 3 entries: + * The first entry defines that for feature flag `my_feature`, the key `key` returns the treatment `on` and the `on` treatment is tied to the configuration `{"desc" : "this applies only to ON treatment"}`. + * The second entry defines that the feature flag `some_other_feature` always returns the `off` treatment and no configuration. + * The third entry defines that `my_feature` always returns `off` for all keys that don't match another entry (in this case, any key other than `key`). + + +## Manager + +Use the Split Manager to get a list of feature flags available to the Split client. + +To instantiate a Manager in your code base, use the same factory that you used for your client. + +```php title="Manager" +manager(); +``` + +The Manager then has the following methods available. + +```php title="PHP" + ... + 'cache' => ... + 'impressionListener' => new CustomImpressionListener(), +); + +$splitFactory = \SplitIO\Sdk::factory('YOUR_SDK_KEY', $sdkConfig); +$splitClient = $splitFactory->client(); +``` + +## Logging + +The Split SDK provides a custom logger that implements the [PSR-3 standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md). **By default, the SDK logs to syslog and a WARNING log level.** To configure the logger, set the adapter and the desired log level. + +:::warning[Production environments] +For production environments, we strongly recommend setting the adapter to **syslog** and avoid using the `echo` adapter. Even better, set your own custom adapter. See [Custom logging](#custom-logging) below. +::: + +```php title="PHP - Logger configuration" + array('adapter' => 'syslog', 'level' => 'error') +); + +$splitFactory = \SplitIO\Sdk::factory('YOUR_SDK_KEY', $sdkConfig); +$splitClient = $splitFactory->client(); +``` + +The log configuration parameters are described below. + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| adapter | The logger adapter. Split SDK supports:
  • **stdout:** Write log messages to standard output (php://stdout)
  • **syslog:** Generate a log message that is distributed by the system logger.
  • **void:** Prevent log writes.
  • **echo:** Echo messages to output. Note that the output could be the web browser.
| syslog | +| level | The log level message. According the [PSR-3 standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md) the supported levels are:
  • emergency
  • alert
  • critical
  • error
  • warning
  • notice
  • info
  • debug
| warning | +| psr3-instance | Your custom logger instance that implements the [PSR-3 standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md) | null + +#### Custom logging + +To integrate a third-party logger, follow this example of including the Zend logger. + +```php title="PHP" + ['psr3-instance' => $psrLogger], +]; + +/** Create the Split client instance. */ +$splitFactory = \SplitIO\Sdk::factory('YOUR_SDK_KEY', $options); +$splitClient = $splitFactory->client(); +``` + +Another example is [Monolog](https://github.com/Seldaek/monolog). Monolog sends your logs to files, sockets, inboxes, databases, and various web services. See the complete list of handlers in the [Monolog documentation](https://github.com/Seldaek/monolog/blob/master/doc/02-handlers-formatters-processors.md). + +```php title="PHP" +pushHandler(new StreamHandler('path/to/your.log', Logger::WARNING)); + +/** SDK options */ +$options = [ + 'log' => ['psr3-instance' => $psrLogger], +]; + +/** Create the Split Client instance. */ +$splitClient = \SplitIO\Sdk::factory('YOUR_SDK_KEY', $options); +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/php-thin-client-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/php-thin-client-sdk.md new file mode 100644 index 00000000000..b499d9c77a6 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/php-thin-client-sdk.md @@ -0,0 +1,442 @@ +--- +title: PHP Thin Client SDK +sidebar_label: PHP Thin Client SDK +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +This guide provides detailed information about our PHP Thin SDK. All of our SDKs are open source. Go to our [PHP Thin SDK GitHub repository](https://github.com/splitio/php-thin-client) to learn more. + +## Language support + +The PHP Thin SDK supports PHP language version 7.3 and later. + +## Architecture + +The PHP Thin SDK depends on the [Split Daemon (splitd)](https://help.split.io/hc/en-us/articles/18305269686157) which should be set up on the same host. The PHP Thin SDK client uses splitd to maintain the local cached copy of the Split rollout plan and return feature flag evaluations. + +## Initialization + +### 1. Import the SDK into your project + +```php title="PHP" +composer require splitsoftware/thin-sdk:1.5.0 +``` + +The public release of the PHP Thin SDK is available at [packagist.org](https://packagist.org/packages/splitsoftware/thin-sdk). + +### 2. Set up the splitd service + +When the composer is done, follow the guidance of our [Split Daemon (splitd)](https://help.split.io/hc/en-us/articles/18305269686157) doc to integrate splitd into your application infrastructure. + +### 3. Instantiate the SDK and create a new split client + +We recommend instantiating the Split factory once as a singleton and reusing it throughout your application. + +```php title="PHP" + ['address' => 'path/to/socket/file.sock'], + 'logging' => ['level' => \Psr\Log\LogLevel::INFO], +]); + +$client = $factory->client(); +``` + +## Using the SDK + +### Basic use + +After you instantiate the SDK client, you can start using the `getTreatment` method of the SDK client to decide what version of your features your customers are served. The method requires the `FEATURE_FLAG_NAME` attribute that you want to ask for a treatment and a unique `key` attribute that corresponds to the end user that you want to serve the feature to. + +From there, you simply need to use an if-else-if block as shown below and insert the code for the different treatments that you defined in the Split user interface. Remember the final else branch in your code to handle the client returning [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). + +```php title="PHP" +getTreatment('key', 'bucketingKey', 'FEATURE_FLAG_NAME', null); + +if ($treatment === 'on') { + // insert code here to show on treatment +} elseif ($treatment === 'off') { + // insert code here to show off treatment +} else { + // insert your control treatment code here +} +``` + +### Attribute syntax + +To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK's `getTreatment` method needs to pass an attribute map at runtime. + +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in the Split Web Console to decide whether to show the `on` or `off` treatment to this account. + +The `getTreatment` method supports five types of attributes: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: + +* **Strings:** Use type String. +* **Numbers:** Use type Integer. +* **Dates:** Express the value in `seconds since epoch`. Use a timestamp represented by an Integer. +* **Booleans:** Use type Boolean. +* **Sets:** Use type Array. + +```php title="PHP" +getTimestamp(); +$attributes["deal_size"] = 10000; +$attributes["paying_customer"] = True; +$attributes["permissions"] = array("gold","silver","platinum"); + +$treatment = $client->getTreatment('key', 'bucketingKey', 'FEATURE_FLAG_NAME', $attributes); + +if ($treatment === 'on') { + // insert code here to show on experience +} elseif ($treatment === 'off') { + // insert code here to show off experience +} else { + // insert your control treatment code here to show no reporting +} +``` + +### Multiple evaluations at once + +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` from the Split client to do this. +* `getTreatments`: Pass a list of the feature flag names you want treatments for. +* `getTreatmentsByFlagSet`: Evaluate all flags that are part of the provided set name and are cached on the SDK instance. +* `getTreatmentsByFlagSets`: Evaluate all flags that are part of the provided set names and are cached on the SDK instance. + + + +```php +$treatments = $client->getTreatments('key', 'bucketingKey', ['FEATURE_FLAG_NAME_1', 'FEATURE_FLAG_NAME_2'], null); + +echo json_encode($treatments); +``` + + +```php +$treatments = $client->getTreatmentsByFlagSet('key', 'bucketingKey', 'backend', null); + +echo json_encode($treatments); +``` + + + +```php +$treatments = $client->getTreatmentsByFlagSets('key', 'bucketingKey', ['backend', 'server_side'], null); + +echo json_encode($treatments); +``` + + + +You can also use the [Split Manager](#manager) to get all of your treatments at once. + +### Get Treatments with Configurations + +To [leverage dynamic configurations with your treatments](https://help.split.io/hc/en-us/articles/360026943552), you should use the `getTreatmentWithConfig` method. This method returns an object containing the treatment and associated configuration. + +The config element is a stringified version of the configuration JSON defined in the Split user interface. If there is no configuration defined for a treatment, the SDK returns `null` for the config parameter. + +This method takes the exact same set of arguments as the standard `getTreatment` method. See below for examples on proper usage: + +```php title="PHP" +$result = $splitClient->getTreatmentWithConfig("KEY", null, "FEATURE_FLAG_NAME", attributes); +$config = json_decode($result["config"], true); +$treatment = $result["treatment"]; +``` + +If you need to get multiple evaluations at once, you can also use the `getTreatmentsWithConfig` methods. These methods take the exact same arguments as the [`getTreatments`](#multiple-evaluations-at-once) methods but return a mapping of feature flag names to SplitResult instead of strings. See example usage below: + +```php title="PHP" +$TreatmentResults = $splitClient->getTreatmentsWithConfig("KEY", null, ["FEATURE_FLAG_NAME_1", "FEATURE_FLAG_NAME_2"], attributes); +// TreatmentResults will have the following form: +// { +// FEATURE_FLAG_NAME_1: {treatment: 'on', +// config: "{ 'color' : 'red'}}", +// FEATURE_FLAG_NAME_2: {treatment: 'v2', +// config: "{ 'copy' : 'better copy'}}", +// } +``` + +### Shutdown + +Due to the nature of PHP and the way HTTP requests are handled, the client is instantiated on every request and automatically destroyed when the request lifecycle comes to an end. The data is synchronized by an external tool and stored in memory, so the SDK client does not need to invoke any shutdown tasks. + +## Track + +Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users’ actions and metrics. + +Refer to the [Events](https://help.split.io/hc/en-us/articles/360020585772) documentation for more information about using track events in feature flags. + +In the examples below you can see that the `.track()` method can take up to five arguments. The proper data type and syntax for each are: + +* **key:** The `key` variable used in the `getTreatment` call and firing this track event. The expected data type is **String**. +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: + * Contains 63 characters or fewer. + * Starts with a letter or number. + * Contains only letters, numbers, hyphen, underscore, or period. + * This is the regular expression we use to validate the value:
`[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` +* **VALUE:** (Optional) The value to be used in creating the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is **Integer** or **Float**. +* **PROPERTIES:** (Optional) A Map of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. + +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the event was successfully queued to be sent back to Split's servers on the next event post. The SDK will return `false` if the current queue on the Split Daemon is full or if an incorrect input to the `track` method has been provided. + +In the case that a bad input has been provided, you can read more about our SDK's expected behavior in the [Events documentation](https://help.split.io/hc/en-us/articles/360020585772-Track-events) + +```php title="PHP 7.3+" +track("KEY", "TRAFFIC_TYPE", "EVENT_TYPE", null, null); +// Example +$trackEvent = $splitClient->track("john@doe.com", "user", "page_load_time", null, null); + +// If you would like to associate a value to an event +$trackEvent = $splitClient->track("KEY", "TRAFFIC_TYPE", "EVENT_TYPE", VALUE, null); +// Example +$trackEvent = $splitClient->track("john@doe.com", "user", "page_load_time", 83.334, null); + +// If you would like to associate just properties to an event +$trackEvent = $splitClient->track("KEY", "TRAFFIC_TYPE", "EVENT_TYPE", null, {PROPERTIES}); + +// If you would like to associate a value and properties to an event +$trackEvent = $splitClient->track("KEY", "TRAFFIC_TYPE", "EVENT_TYPE", VALUE, {PROPERTIES}); +// Example +$properties = array( + "package" => "premium", + "admin" => true, + "discount" => 50 +); +$trackEvent = $splitClient->track("KEY", "TRAFFIC_TYPE", "EVENT_TYPE", 83.334, $properties); +``` + +## Configuration + +With the SDK architecture, there is a set of options that you can configure to get everything connected and working as expected. + +| **Option** | **Description** | +| --- | --- | +| transfer | IPC socket parameters (type, address, timeouts) | +| logging | Logging parameters | +| utils | Instance of an impression listener to send impression data to a custom location | + +```php title="PHP" + ['address' => '/var/run/splitd.sock'], + 'logging' => ['psr-instance' => $myLogger], +]; + +$splitFactory = \SplitIO\ThinSdk\Factory::withConfig($sdkConfig); +$splitClient = $splitFactory->client(); +``` + +### Impressions data + +By default, the SDK sends small amounts of information to the Split backend indicating the reason for each treatment returned from a feature flag. An example would be that a user saw the `on` treatment because they are `in segment all`. + +## Manager + +Use the Split Manager to get a list of feature flags available to the Split client. + +To instantiate a Manager in your code base, use the same factory that you used for your client. + +```php title="Manager" +manager(); +``` + +The Manager then has the following methods available. + +```php title="PHP" +getKey() + ." feat=".$i->getFeature() + ." treatment=".$i->getTreatment() + ." label=".$i->getLabel() + ." cn=".$i->getChangeNumber() + ." #attrs=".(($a == null) ? 0 : count($a))."\n"; + } +} +``` + +### Attach a custom impression listener + +Here is an example of how to attach a custom impression listener. + +```php title="PHP" +$sdkConfig = [ + 'transfer' => ['address' => '/var/run/splitd.sock'], + 'logging' => ['psr-instance' => $myLogger], + 'utils' => ['impressionListener' => new \App\Utils\CustomListener()], ## <-- custom listener +]; + +$splitFactory = \SplitIO\ThinSdk\Factory::withConfig($sdkConfig); +$splitClient = $splitFactory->client(); +``` + +## Logging + +The Split SDK provides a custom logger that implements the [PSR-3 standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md). **By default, the SDK logs to stdout at the INFO log level.** To configure the logger, set the adapter and the desired log level. + +:::warning[Production environments] +For production environments, we strongly recommend passing a proper `psr-instance` parameter with a PSR3 compliant logger (or custom wrapper for a non-cmpliant one). +::: + +```php title="PHP - Logger configuration" + ['level' => \Psr\Log\LogLevel::DEBUG], +); + +$splitFactory = \SplitIO\ThinSdk\Factory::withConfig($sdkConfig); +$splitClient = $splitFactory->client(); +``` + +The log configuration parameters are described below. + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| level | The log level message. According the [PSR-3 standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md) the supported levels are:
  • emergency
  • alert
  • critical
  • error
  • warning
  • notice
  • info
  • debug
  • info
+| psr-instance | Your custom logger instance that implements the [PSR-3 standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md) | null + +#### Custom logging + +You can integrate a third-party logger to format logging or to push logging info to a configurable location. + +##### Zend logger + +The [Zend logger](https://docs.zendframework.com/zend-log/) can be integrated with the SDK as shown below. + +```php title="PHP" + ['psr-instance' => $psrLogger], +]; + +/** Create the Split Client instance. */ +$splitFactory = \SplitIO\ThinSdk\Factory::withConfig($sdkConfig); +$splitClient = $splitFactory->client(); +``` + +##### Monolog + +[Monolog](https://github.com/Seldaek/monolog) sends your logs to files, sockets, inboxes, databases, and various web services. See the complete list of handlers in the [Monolog documentation](https://github.com/Seldaek/monolog/blob/master/doc/02-handlers-formatters-processors.md). + +The following is a demonstration of Monolog integration for the SDK. + +```php title="PHP" +pushHandler(new StreamHandler('path/to/your.log', Logger::WARNING)); + +/** SDK options */ +$sdkConfig = [ + 'logging' => ['psr-instance' => $psrLogger], +]; + +/** Create the Split Client instance. */ +$splitFactory = \SplitIO\ThinSdk\Factory::withConfig($sdkConfig); +$splitClient = $splitFactory->client(); +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/python-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/python-sdk.md new file mode 100644 index 00000000000..fc14878358f --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/python-sdk.md @@ -0,0 +1,1546 @@ +--- +title: Python SDK +sidebar_label: Python SDK +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +This guide provides detailed information about our Python SDK. All of our SDKs are open source. Go to our [Python SDK GitHub repository](https://github.com/splitio/python-client) to learn more. + +## Language support + +The Python SDK supports Python 3 (3.7.16 or later). + +## Multi-thread, multi-process and asyncio modes support + +One of Python's great built-in features is the ability to parallelize your code to optimize the execution-performance of any module. You can implement your project in a multi-threaded, multi-process or asyncio mode, depending on what works best for you and your team. + +Please note, multiple processes in Python are unable to share memory space, so the setup and instantiation process is different for each mode. + +Jump to the setup process for the mode your application is built in: + +* [Multi-threaded SDK initialization](#initialization-multi-threaded-mode) +* [asyncio SDK initialization](#initialization-asyncio-mode) +* [Multi-process SDK initialization](#initialization-multi-process-mode) + +(Note: Django projects are multi-process by default) + +## Initialization: Multi-threaded mode + +Set up Split in your code base with two simple steps. + +### 1. Import the SDK into your project using pip + +```bash title="Shell" +pip install 'splitio_client[cpphash]==10.2.0' +``` + +### 2. Instantiate the SDK and create a new split client + +:::danger[If upgrading an existing SDK - Block until ready changes] +Starting in version `8.0.0`, readiness has been migrated to a two part implementation. See below for syntax changes you must make if upgrading your SDK to the newest version. +::: + +When the SDK is instantiated in `in-memory` mode, it kicks off background tasks to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while its in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK doesn't fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). + +To make sure the SDK is properly loaded before asking it for a treatment, block until the SDK is ready. +Since version `8.0.0` This is done by calling the `.block_until_ready()` method in the factory object. +This method also accepts a maximum time (in seconds or fractions of it) to wait until the SDK is ready, or throw an exception in case it's not. + +We recommend instantiating the Split factory once as a singleton and reusing it throughout your application. + +Configure the SDK with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split user interface, on your Admin settings page, API keys section. Select a server-side SDK API key. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. + +```python title="Python" +from splitio import get_factory +from splitio.exceptions import TimeoutException + +factory = get_factory('YOUR_SDK_KEY') +try: + factory.block_until_ready(5) ## wait up to 5 seconds +except TimeoutException: + ## Now the user can choose whether to abort the whole execution, or just keep going + ## without a ready client, which if configured properly, should become ready at some point. + pass +split = factory.client() +``` + +Now you can start asking the SDK to evaluate treatments for your customers. + +## Initialization: asyncio mode + +Python's asyncio library had gather lot of attention and support and provides many advantages to multi-threaded programming especially in I/O operations, checkout the [official doc](https://docs.python.org/3/library/asyncio.html) for more info. + +Set up Split in your code base with two simple steps. + +### 1. Import the SDK into your project using pip + +```bash title="Shell" +pip install 'splitio_client[cpphash,asyncio]==10.2.0' +``` + +### 2. Instantiate the SDK and create a new split client + +:::danger[asyncio support] +Starting in version `10.0.0`, SDK support asyncio library, this required a breaking change to upgrade the python supported version to be 3.7.16 or later. +::: + +:::info[asyncio support] +When using the SDK, regardless if the mode is asyncio or Multi-threaded, all the public SDK API are identical, with only one exception; when initializing the factory. +::: + +Similar to Multi-threaded mode, when the SDK is instantiated in `in-memory`, it kicks off background asyncio tasks to update an in-memory cache with small amounts of data fetched from Split servers. To make sure the SDK cache is properly loaded before asking it for a treatment, utilize `block_until_ready()` method. + +We recommend instantiating the SDK once as a singleton and reusing it throughout your application. + +Use the code snippet below and plug in your API key. The API key is available on your **Organization Settings** page, on the **APIs** tab. The API key is of type `sdk`. For more information, see [Understanding API Keys](https://help.split.io/hc/en-us/articles/360019916211-API-keys). + +```python title="Python" +from splitio import get_factory_async +from splitio.exceptions import TimeoutException + +async def main(): + factory = await get_factory_async('YOUR_SDK_KEY') + try: + await factory.block_until_ready(5) ## wait up to 5 seconds + except TimeoutException: + ## Now the user can choose whether to abort the whole execution, or just keep going + ## without a ready client, which if configured properly, should become ready at some point. + pass + split = factory.client() + +loop = asyncio.new_event_loop() +loop.run_until_complete(main()) +``` + +For the following sections, please lookup the `asyncio` tab in each code example block. + +## Initialization: Multi-process mode + +There are a few extra steps for setting up our SDK with Python in multi-process mode, described below. Before hopping into the details, we will quickly review the multi-process mode setup differences. + +### SDK architecture + +When the application is run in a server that spawns multiple processes (workers) to handle HTTP requests, all of them need to access fetched feature flags and segments as well as queuing up impressions and events. Since processes cannot access each other's memory, using the standalone operation mode will result in several sets of synchronisation tasks (threads) doing the same job (at least one per http worker - possibly more, since workers are often restarted). +To avoid this scenario, the Split.IO SDK for Python supports an alternative operation mode, which uses an external tool called `Split-Synchronizer` and a `redis` cache. Our synchronization tool is responsible for maintaining the split data updated and flushing impressions, events and metrics to the split servers. +If you are using a preforked-type server such as uWSGI or GUnicorn, we also offer a series of methods that can be attached to the server's "post-fork" hooks in order to ensure synchronization runs properly on the worker process after the master is forked. + +The previously mentioned approaches are described in depth below: + +* [Redis cache and client setup](#redis-cache-and-client-setup) +* [Preforked client setup](#preforked-client-setup) + +### Redis cache and client setup + +Before you get started with the cache, download the correct version of Redis to your machine. Our SDK Redis integration requires a Redis version `2.10.5` or later. Also want to make sure to start your Redis server. Refer to the [Redis documentation](https://redis.io/topics/quickstart) for help. After that, there are a few more steps to set up the cache with Redis. + +#### 1. Install the Split SDK into your project + +Use `pip install` to install the SDK. Note that the package is different for standard Python and for Django, as shown below. + + + +```bash +pip install 'splitio_client[redis,cpphash]==10.2.0' +``` + + +```bash +pip install 'splitio_client[redis,cpphash,asyncio]==10.2.0' +``` + + +```bash +pip install django_splitio[redis]==2.4.0 +``` + + + +:::warning[If using Synchronizer with Redis - Synchronizer 2.x required for SDK Version `7.0` and onwards] +Since version `2.0.0` of the split-synchronizer, we use a more efficient scheme to store impressions in Redis. This approach is faster and easier on your Redis instances, since it yields better throughput of impressions to the backend. If you use this SDK with the Synchronizer in Redis or Proxy mode, you will need the newest versions of our Split Synchronizer. It is recommended that once you're using SDK versions compatible with Split-Sync 2.0 on all your applications pointing to the redis instance maintained by the Split-Sync, you disable backwards compatibility (this is as easy as changing a parameter to `true` on the JSON config or an environment variable to `on` if you're using the docker image). +::: + +#### 2. Set up the Split Synchronizer + +Set up the [Split Synchronizer](https://help.split.io/hc/en-us/articles/360019686092) to sync data to a Redis cache. Follow the steps in the [set up article](https://help.split.io/hc/en-us/articles/360019686092), then come back to this doc and go to step 3 to instantiate the client, below. + +#### 3. Instantiate the SDK client with Redis enabled + +If you are using Django, there is one extra step to add `django_splitio` to `INSTALLED_APPS` in your Django settings and add a SPLITIO dictionary in the Django settings. Input your own SDK key in for `YOUR_SDK_KEY`. + +To instantiate the SDK client, copy and paste the code snippet below into your code base where you want to use Split to roll out your feature flag. Again, note that the syntax is different for standard Python and for Django. + + + +```python +from splitio import get_factory + +config = { + 'redisHost' : 'localhost', + 'redisPort' : 6379, + 'redisDb' : 0, + 'redisPassword' : 'somePassword', + ## if the user access is not the default 'root' user, inlcude parameter below + 'redisUsername' : 'username', + ## if you've set a redis prefix also include that in the config + 'redisPrefix' : 'your prefix that you defined' +} + +factory = get_factory('YOUR_SDK_KEY', config=config) +split = factory.client() +``` + + +```python +from splitio import get_factory_async + +async def main(): + config = { + 'redisHost' : 'localhost', + 'redisPort' : 6379, + 'redisDb' : 0, + 'redisPassword' : 'somePassword', + ## if the user access is not the default 'root' user, inlcude parameter below + 'redisUsername' : 'username', + ## if you've set a redis prefix also include that in the config + 'redisPrefix' : 'your prefix that you defined' + } + + factory = await get_factory_async('YOUR_SDK_KEY', config=config) + split = factory.client() + +loop = asyncio.new_event_loop() +loop.run_until_complete(main()) +``` + + +```python +## In your django config: +INSTALLED_APPS = ( + ... + 'django_splitio', + ... + ) + +SPLITIO = { + 'apiKey': 'YOUR_SDK_KEY', + 'labelsEnabled': True, + 'redisHost': 'localhost', + 'redisPort': 6379, + 'redisDb': 0, + 'redisPassword': 'somePassword' + ## if the user access is not the default 'root' user, inlcude parameter below + 'redisUsername' : 'username', + ## if you've set a redis prefix also include that in the config + 'redisPrefix' : 'your prefix that you defined' +} + +## ------------------------- + +## in any module where the sdk is to be used. +from django_splitio import get_factory + +factory = get_factory() +client = factory.client() +``` + + + +Now you can start asking the SDK to evaluate treatments for your customers. + +#### Redis Sentinel + +The SDK also supports Redis with Sentinel (v2) replication. The client can be configured to operate with single master/multiple slaves to provide high availability. The current version of Sentinel is `2`. A stable release of Sentinel has been shipped since Redis `2.8`. For further information about Sentinel, refer to the [Sentinel documentation](https://redis.io/topics/sentinel). + +Use the following configuration for Redis in Sentinel mode. + +| **Variable** | **Type** | **Description** | +| --- | --- | --- | +| redisSentinels | array | The list of sentinels for replication service. | +| redisMasterService | string | The name of master service. | + + + +```python +from splitio import get_factory + +config = { + 'redisDb': 0, + 'redisPrefix': '', + 'redisSentinels': [('SENTINEL_HOST_1', SENTINEL_PORT_1), ('SENTINEL_HOST_2', SENTINEL_PORT_2), ('SENTINEL_HOST_3', SENTINEL_PORT_3)], + 'redisMasterService': 'SERVICE_MASTER_NAME', + 'redisSocketTimeout': 5 +} + +factory = get_factory('SDK_KEY', config=config) +split = factory.client() +``` + + +```python +from splitio import get_factory_async + +async def main(): + config = { + 'redisDb': 0, + 'redisPrefix': '', + 'redisSentinels': [('SENTINEL_HOST_1', SENTINEL_PORT_1), ('SENTINEL_HOST_2', SENTINEL_PORT_2), ('SENTINEL_HOST_3', SENTINEL_PORT_3)], + 'redisMasterService': 'SERVICE_MASTER_NAME', + 'redisSocketTimeout': 5 + } + + factory = await get_factory_async('SDK_KEY', config=config) + split = factory.client() + +loop = asyncio.new_event_loop() +loop.run_until_complete(main()) +``` + + +```python +## In your django config: +INSTALLED_APPS = ( + ... + 'django_splitio', + ... + ) + +SPLITIO = { + 'apiKey': 'YOUR_SDK_KEY', + 'labelsEnabled': True, + 'redisSentinels': [('SENTINEL_HOST_1', SENTINEL_PORT_1), ('SENTINEL_HOST_2', SENTINEL_PORT_2), ('SENTINEL_HOST_3', SENTINEL_PORT_3)], + 'redisMasterService': 'SERVICE_MASTER_NAME', + 'redisDb': 0, + 'redisPassword': 'somePassword', + 'redisSocketTimeout': 5 + ## if the user access is not the default 'root' user, inlcude parameter below + 'redisUsername' : 'username' +} + +## ------------------------- + +## in any module where the sdk is to be used. +from django_splitio import get_factory + +factory = get_factory() +client = factory.client() +``` + + + +#### Redis Cluster + +This functionality is currently not supported for this SDK, but is planned for a future release. Subscribe to our [release notes](https://www.split.io/releases) for updates. + +### Preforked client setup + +Since version `8.4.0` we added support for running our SDK in standalone mode in preforked multiprocess servers. With this feature you can take advantage of using Split in preforking servers such as GUnicorn or uWSGI and attaching it to the `postfork` hooks. This can yield significant performance improvements in terms of memory in comparison to use lazy-style initialization and greatly reduced evaluation time in comparison to use Redis + Split Synchronizer approach at the expense of CPU and BG network traffic. +There are two main steps for initializating the Split SDK by using hooks: +1. `preforkedInitialization`: this is a new configuration option that will tell the SDK that it should initiate the SDK in master mode and it will not start polling nor streaming. +2. `factory.resume()`: this is a new method provided by Split Factory that should be executed on newly forked http worker processes in order to resume synchronisation. + +:::warning +Preforked client is not supported in asyncio mode. +::: + +#### Example using uWSGI preforked server + +#### Adding postfork handler + +There are a few extra steps to set up SDK with `postfork` option. +1. Importing the `uwsgidecorators` module for handling hooks. +2. Set `preforkedInitialization` as true in the sdk configs. +3. Add and use the `postfork` decorator. +5. Call `factory.resume()` method to resume Split tasks on each forked child process. + +**Note:** Make sure to add the parameter `--enable-threads` to enable multi-threading when starting the UWSGI app server. While Python SDK does support UWSGI app server in process based mode, for the SDK to synchronize with Split cloud, you need to enable the multi-threading option, as the background threads perform the synching task. For example: + +```bash title="Shell" +uwsgi --http :8080 --chdir /var/app --wsgi-file ${WSGI_PATH} ${UWSGI_MODULE} --master +--processes ${UWSGI_NUM_PROCESSES} --uid ${UWSGI_UID} --gid ${UWSGI_GID} -t ${UWSGI_TIMEOUT} +--http-keepalive --add-header ${UWSGI_HEADERS} --buffer-size ${UWSGI_BUFFER_SIZE} +--enable-threads +``` + + + +```python +import logging +import uwsgi +from uwsgidecorators import postfork ## Step 1 + + +logging.basicConfig(level=logging.DEBUG) + +## more code ... + +SPLIT = get_factory( + 'YOUR_SDK_KEY', + config={ + 'preforkedInitialization': True, ## Step 2 + }, +) + + +@postfork ## Step 3 +def post_fork_execution(): + SPLIT.resume() ## Step 4 + SPLIT.block_until_ready(5) + +## more code ... +``` + + +```python +## In your django config: +INSTALLED_APPS = ( + ... + 'django_splitio', + ... + ) + SPLITIO = { + 'apiKey': 'YOUR_SDK_KEY', + 'preforkedInitialization': True ## Step 2 + } +## ------------------------- +## in setup Split module +from django_splitio import get_factory + + +global SPLIT + + +def setup(): + global SPLIT + if 'SPLIT' in globals(): + return + SPLIT = get_factory() + +## in master module +import logging +import uwsgi +from uwsgidecorators import postfork ## Step 1 +from django_splitio_testapp.split_wrapper import setup + + +logging.basicConfig(level=logging.DEBUG) +setup() + + +@postfork ## Step 3 +def post_fork(): + from django_splitio_testapp.split_wrapper import SPLIT + SPLIT.resume() ## Step 4 + SPLIT.block_until_ready(5) + +## more code ... + +``` + + + +For further reading about uwsgi decorators and postfork you can take a look at the [official documentation](https://uwsgi-docs.readthedocs.io/en/latest/PythonDecorators.html#uwsgidecorators.postfork) + +## Using the SDK + +### Basic use + +After you instantiate the SDK client, you can start using the `get_treatment` method of the SDK client to decide what version of your feature flags your customers are served. The method requires the `FEATURE_FLAG_NAME` attribute that you want to ask for a treatment and a unique `key` attribute that corresponds to the end user that you want to serve the feature to. + +From there, you simply need to use an if-else-if block as shown below and insert the code for the different treatments that you defined in the Split UI. Remember the final else branch in your code to handle the client returning [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). + + + +```python +## The key here represents the ID of the user/account/etc you're trying to evaluate a treatment for +treatment = split.get_treatment('key', 'FEATURE_FLAG_NAME') + +if treatment == "on": + ## insert code here to show on treatment +elif treatment == "off": + ## insert code here to show off treatment +else: + ## insert your control treatment code here +``` + + +```python +## The key here represents the ID of the user/account/etc you're trying to evaluate a treatment for +treatment = await split.get_treatment('key', 'FEATURE_FLAG_NAME') + +if treatment == "on": + ## insert code here to show on treatment +elif treatment == "off": + ## insert code here to show off treatment +else: + ## insert your control treatment code here +``` + + + +:::info[key should be String] +If the `key` attribute is something other than `string`, Python SDK returns `CONTROL` after evaluation. +::: + +### Attribute syntax + +To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK's `get_treatment` method needs to be passed an attribute map at runtime. + +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `get_treatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in the Split user interface to decide whether to show the `on` or `off` treatment to this account. + +The `get_treatment` method supports five types of attributes: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: + +* **Strings:** Use type String. +* **Numbers:** Use type Integer. +* **Dates: ** Express the value as the number of seconds since the epoch as seconds in UTC. For example, attribute `registered_date` is `arrow.utcnow().timestamp`, which is an integer. +* **Booleans:** Use type Boolean. +* **Sets:** Use type Set. + + + +```python +import arrow +from splitio import get_factory + +factory = get_factory('YOUR_SDK_KEY') +split = factory.client() + +attributes = dict() +attributes['plan_type'] = 'growth' +attributes['registered_date'] = arrow.utcnow().timestamp +attributes['deal_size'] = 1000 +attributes['paying_customer'] = True + +treatment = split.get_treatment("key", "FEATURE_FLAG_NAME", attributes) + +if treatment == "on": + ## insert on code here +elif treatment == "off": + ## insert off code here +else: + ## insert control code here +``` + + +```python +import arrow +from splitio import get_factory_async + +async def main(): + factory = await get_factory_async('YOUR_SDK_KEY') + split = factory.client() + + attributes = dict() + attributes['plan_type'] = 'growth' + attributes['registered_date'] = arrow.utcnow().timestamp + attributes['deal_size'] = 1000 + attributes['paying_customer'] = True + + treatment = await split.get_treatment("key", "FEATURE_FLAG_NAME", attributes) + + if treatment == "on": + ## insert on code here + elif treatment == "off": + ## insert off code here + else: + ## insert control code here + +loop = asyncio.new_event_loop() +loop.run_until_complete(main()) +``` + + + +### Multiple evaluations at once + +In some instances, you may want to evaluate treatments for multiple feature flag at once. Use the different variations of `get_treatments` method from the split client to do this. +* `get_treatments`': Pass a list of the feature flag names you want treatments for. +* `get_treatments_by_flag_set`: Evaluate all flags that are part of the provided set name and are cached on the SDK instance. +* `get_treatments_by_flag_sets`: Evaluate all flags that are part of the provided set names and are cached on the SDK instance. + +**Multi-threaded** + + + +```python +treatments = split.get_treatments('key', ['FEATURE_FLAG_NAME_1', 'FEATURE_FLAG_NAME_2']) + +print(treatments) +``` + + +```python +treatments = split.get_treatments_by_flag_set('key', 'backend') + +print(treatments) +``` + + +```python +treatments = split.get_treatments_by_flag_sets('key', ['backend', 'server_side']) + +print(treatments) +``` + + + +**asyncio** + + + +```python +treatments = await split.get_treatments('key', ['FEATURE_FLAG_NAME_1', 'FEATURE_FLAG_NAME_2']) + +print(treatments) +``` + + +```python +treatments = await split.get_treatments_by_flag_set('key', 'backend') + +print(treatments) +``` + + +```python +treatments = await split.get_treatments_by_flag_sets('key', ['backend', 'server_side']) + +print(treatments) +``` + + + +### Get Treatments with Configurations + +To [leverage dynamic configurations with your treatments](https://help.split.io/hc/en-us/articles/360026943552), you should use the `get_treatment_with_config` method. + +This method will return an object containing the treatment and associated configuration. + +The config element will be a stringified version of the configuration JSON defined in the Split user interface. If there are no configs defined for a treatment, the SDK returns `None` for the config parameter. + +This method takes the exact same set of arguments as the standard `get_treatment` method. See below for examples on proper usage: + + + +```python + +treatment, raw_config = client.get_treatment_with_config('key', 'FEATURE_FLAG_NAME', attributes) +configs = json.loads(raw_config) + +if treatment == 'on': + ## insert on code here and use configs here as necessary +else if treatment == 'off': + ## insert off code here and use configs here as necessary +else: + ## insert control code here +``` + + +```python + +treatment, raw_config = await client.get_treatment_with_config('key', 'FEATURE_FLAG_NAME', attributes) +configs = json.loads(raw_config) + +if treatment == 'on': + ## insert on code here and use configs here as necessary +else if treatment == 'off': + ## insert off code here and use configs here as necessary +else: + ## insert control code here +``` + + + +If you need to get multiple evaluations at once, you can also use the `get_treatments_with_config` methods. +These methods take the exact same arguments as the [get_treatments](#multiple-evaluations-at-once) methods but return a mapping of feature flag names to SplitResult objects instead of strings. Example usage below: + +**Multi-threaded** + + + +```python +feature_flag_names = ['FEATURE_FLAG_1', 'FEATURE_FLAG_2'] +split_results = client.get_treatments_with_config('key', feature_flag_names) + + ## split_results will have the following form: + ## { + ## 'FEATURE_FLAG_1': ('on', '{"color": "red"}'), + ## 'FEATURE_FLAG_2': ('v2', '{"copy": "better copy"}') + ## } +``` + + +```python +attributes = {} +result = split.get_treatments_with_config_by_flag_set('key', 'backend', attributes) +for feature_flag, treatment_with_config in result.items(): + treatment = treatment_with_config[0] + configs = treatment_with_config[1] + print("Feature: %s, Treatment: %s, Config: %s" % (feature_flag, treatment, configs)) +``` + + +```python +attributes = {} +result = split.get_treatments_with_config_by_flag_sets('key', 'backend', attributes) +for feature_flag, treatment_with_config in result.items(): + treatment = treatment_with_config[0] + configs = treatment_with_config[1] + print("Feature: %s, Treatment: %s, Config: %s" % (feature_flag, treatment, configs)) +``` + + + +**asyncio** + + + +```python +feature_flag_names = ['FEATURE_FLAG_1', 'FEATURE_FLAG_2'] +split_results = await client.get_treatments_with_config('key', feature_flag_names) + + ## split_results will have the following form: + ## { + ## 'FEATURE_FLAG_1': ('on', '{"color": "red"}'), + ## 'FEATURE_FLAG_2': ('v2', '{"copy": "better copy"}') + ## } +``` + + +```python +attributes = {} +result = await split.get_treatments_with_config_by_flag_set('key', 'backend', attributes) +for feature_flag, treatment_with_config in result.items(): + treatment = treatment_with_config[0] + configs = treatment_with_config[1] + print("Feature: %s, Treatment: %s, Config: %s" % (feature_flag, treatment, configs)) +``` + + +```python +attributes = {} +result = await split.get_treatments_with_config_by_flag_sets('key', ['backend'], attributes) +for feature_flag, treatment_with_config in result.items(): + treatment = treatment_with_config[0] + configs = treatment_with_config[1] + print("Feature: %s, Treatment: %s, Config: %s" % (feature_flag, treatment, configs)) +``` + + + +### Shutdown + +The in-memory implementation of Python uses threads in Multi-threaded mode and tasks in asyncio mode to synchronize feature flags, segments, and impressions. If at any point in the application the split client is not longer needed, you can disable it by calling the `destroy()` method on the factory object. + +This does NOT kill the threads or tasks if they are synchronizing, but prevents them from rescheduling for future executions. + +When you call the `.destroy()` method from the client, any subsequent call to `get_treatment()`returns `CONTROL`, and when querying `splits` or `split_names` via the manager interface, an empty list `[]` is returned. + +Since version `8.0.0` .destroy() accepts an optinal argument of type `threading.Event`. This allows the user to have control of the shutdown cycle of the SDK. +The user can for example choose to block the application until destroy() has finished, so that all the impressions and events are flushed correctly before the application shuts down. + + + +```python +stop_event = threading.Event() +factory.destroy(stop_event) +stop_event.wait() +sys.exit(0) +``` + + +```python +await factory.destroy() +sys.exit(0) +``` + + + +:::warning[Important!] +A call to the `destroy()` method also destroys the factory object. When creating new client instance, first create a new factory instance. +::: + +## Track + +Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users’ actions and metrics. + +[Learn more](https://help.split.io/hc/en-us/articles/360020585772) about using track events in splits. + +In the examples below you can see that the `.track()` method can take up to five arguments. The proper data type and syntax for each are: + +* **key:** The `key` variable used in the `get_treatment` call and firing this track event. The expected data type is **String**. +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: + * Contains 63 characters or fewer. + * Starts with a letter or number. + * Contains only letters, numbers, hyphen, underscore, or period. + * This is the regular expression we use to validate the value:
`[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` +* **VALUE:** (Optional) The value to be used in creating the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is **Integer** or **Float**. +* **PROPERTIES:** (Optional) A Map of key value pairs that can be used to filter your metrics. Learn more about event property capture [in the Events guide](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties). Split currently supports three types of properties: strings, numbers, and booleans. + +**Redis Support:** If you are using our SDK with Redis, you need Split Synchronizer **2.3.0** version at least in order to support *properties* in the `track` method. + +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event to be sent back to Split's servers on the next event post. The SDK will return `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method has been provided. + +In the case that a bad input has been provided, you can read more about our SDK's expected behavior [here](https://help.split.io/hc/en-us/articles/360020585772-Track-events) + + + +```python +# If you would like to send an event without a value +trackEvent = client.track("key", "TRAFFIC_TYPE", "EVENT_TYPE") +# Example +trackEvent = client.track("john@doe.com", "user", "page_load_time") + +# If you would like to associate a value to an event +trackEvent = client.track("key", "TRAFFIC_TYPE", "EVENT_TYPE", VALUE) +# Example +trackEvent = client.track("john@doe.com", "user", "page_load_time", 83.334) + +# If you would like to associate just properties to an event +trackEvent = client.track("key", "TRAFFIC_TYPE", "EVENT_TYPE", None, {PROPERTIES}) + +# If you would like to associate a value and properties to an event +trackEvent = client.track("key", "TRAFFIC_TYPE", "EVENT_TYPE", VALUE, {PROPERTIES}) +# Example +properties = { + "package": "premium", + "admin": true, + "discount": 50 +} +trackEvent = client.track("key", "TRAFFIC_TYPE", "EVENT_TYPE", 83.334, properties) +``` + + +```python +# If you would like to send an event without a value +trackEvent = await client.track("key", "TRAFFIC_TYPE", "EVENT_TYPE") +# Example +trackEvent = await client.track("john@doe.com", "user", "page_load_time") + +# If you would like to associate a value to an event +trackEvent = await client.track("key", "TRAFFIC_TYPE", "EVENT_TYPE", VALUE) +# Example +trackEvent = await client.track("john@doe.com", "user", "page_load_time", 83.334) + +# If you would like to associate just properties to an event +trackEvent = await client.track("key", "TRAFFIC_TYPE", "EVENT_TYPE", None, {PROPERTIES}) + +# If you would like to associate a value and properties to an event +trackEvent = await client.track("key", "TRAFFIC_TYPE", "EVENT_TYPE", VALUE, {PROPERTIES}) +# Example +properties = { + "package": "premium", + "admin": true, + "discount": 50 +} +trackEvent = await client.track("key", "TRAFFIC_TYPE", "EVENT_TYPE", 83.334, properties) +``` + + + +## Configuration + +The SDK has a number of knobs for configuring performance. Each knob is tuned to a reasonable default. However, you can override the value while instantiating the SDK. The parameters available for configuration are shown below. + +| **Configuration** | **Description** | **Default value** | **Applies to** | +| --- | --- | --- | --- | +| featuresRefreshRate | The SDK polls Split servers for changes to feature flags at this period (in seconds). | 30 seconds | In-memory. | +| segmentsRefreshRate | The SDK polls Split servers for changes to segments at this period (in seconds). | 30 seconds | In-memory. | +| impressionsRefreshRate | The treatment log captures which customer saw what treatment (on, off, and so on) at what time. This log is periodically flushed back to Split servers. This configuration controls how quickly the cache expires after a write (in seconds). | 300 seconds | In-memory. | +| metricsRefreshRate | The SDK caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers (in seconds). | 3600 seconds | In-memory. | +| eventsPushRate | How often the SDK sends events to the backend. | 10 seconds | In-memory. | +| labelsEnabled | Disable labels from being sent to Split backend. Labels may contain sensitive information. | true | All operation modes. | +| connectionTimeout | HTTP client connection timeout (in ms). | 1500ms | In-memory. | +| apiKey | The Split SDK key. This entry is mandatory. If `localhost` is supplied as the SDK key, a localhost only client is created when `get_client` is called. | None | All operation modes. | +| redisHost | The host that contains the Redis instance. | localhost | Redis-based storage setup. | +| redisPort | The port of the Redis instance. | 6379 | Redis-based storage setup. | +| redisDb | The db index on the Redis instance. | 0 | Redis-based storage setup. | +| redisUsername | The user name for Redis. | None | Redis-based storage setup. | +| redisPassword | The password for Redis. | None | Redis-based storage setup. | +| redisPrefix | The prefix for each key written in Redis by the SDK. | None | Redis-based storage setup. | +| redisSsl | Enable encrypted connections to redis. | False | Redis-based storage setup. | +| redisSslKeyfile | Client key used to decrypt incoming responses. | None | Redis-based storage setup. | +| redisSslCertfile | Client certificate to prove client's identity to the server. | None | Redis-based storage setup. | +| redisSslCertReqs | Whether to validate the server public key or blindly accept it and use it. | None | Redis-based storage setup. | +| redisSslCaCerts | CA Root certificates capable of validating the certificate presented by the server. | None | Redis-based storage setup. | +| redisLocalCacheEnabled | Enable a local in-memory cache on top of redis for fetching feature flags. | True | Redis-based storage setup. | +| redisLocalCacheTTL | How long to cache feature flags in memory (in seconds). | 5 | Redis-based storage setup. | +| redisSocketTimeout | Socket Timeout for Redis. | None | Redis-based storage setup. | +| redisSocketConnectTimeout | Socket Connection Timeout for Redis. | None | Redis-based storage setup. | +| redisRetryOnTimeout | If retries for Redis operations would be performed by the SDK if it receives a timeout. | False | Redis-based storage setup. | +| preforkedInitialization | Flag for enabling fork execution (requires extra setup mentioned on the preforked client setup section). | False | Preforked running mode. | +| impressionListener | Custom implementation of impression listener interface. | None | Redis-based storage setup. | +| eventsQueueSize | Max number of events to accumulate before sending them to the backend. | 10000 | In-memory. | +| eventsBulkSize | How many events to package when submiting them to the split servers | 5000 | In-memory. | +| impressionsQueueSize | Max number of impressions to accumulate before sending them to the backend. | 10000 | In-memory. | +| impressionsBulkSize | How many impressions to package when submiting them to the split servers | 5000 | In-memory. | +| IPAddressesEnabled | Flag to disable IP addresses and host name from being sent to the Split backend. | True | Redis, In-memory. | +| impressionsMode | This configuration defines how impressions are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Split; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. In DEBUG mode, ALL impressions are queued and sent to Split. Use DEBUG mode when you want every impression to be logged in Split's user interface when trying to debug your SDK setup. This setting does not impact the impression listener which will receives all generated impressions. | `'optimized'` | In-memory operation mode. | +| streamingEnabled | Boolean flag to enable the streaming service as default synchronization mechanism. In the event of an issue with streaming, the SDK will fallback to the polling mechanism. If false, the SDK will poll for changes as usual without attempting to use streaming. | True | In-memory operation mode. | +| flagSetsFilter | This setting allows the SDK to only synchronize the feature flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the SDK instance, bringing all the benefits from a reduced payload. | None | + +:::info[Impression listener] +Starting with this version, the SDK removed the `impression_listener` parameter. If this parameter is passed, it is not handled by the SDK. If you want to attach a custom impression listener, send the new `impressionListener` parameter with an implementation of the impression listener interface. +::: + +To set each of the parameters defined above, the syntax should be as seen below. + +Note that if you are using Standard Python, you pass the configuration parameters as a dictionary to the factory. In Django, plug the configuration parameters in to the SPLITIO dictionary in your Django settings + + + +```python +from splitio import get_factory + +configuration = { + 'metricsRefreshRate' : 60, + 'impressionsRefreshRate' : 60, + 'ready' : 0, + 'connectionTimeout' : 1500, + 'readTimeout' : 1500, + 'labelsEnabled' : True +} + +factory = get_factory('YOUR_SDK_KEY', config=configuration) +split = factory.client() +``` + + +```python +from splitio import get_factory_async + +async def main(): + configuration = { + 'metricsRefreshRate' : 60, + 'impressionsRefreshRate' : 60, + 'ready' : 0, + 'connectionTimeout' : 1500, + 'readTimeout' : 1500, + 'labelsEnabled' : True + } + + factory = await get_factory_async('YOUR_SDK_KEY', config=configuration) + split = factory.client() + +loop = asyncio.new_event_loop() +loop.run_until_complete(main()) +``` + + +```python +SPLITIO = { + 'apiKey': 'YOUR_SDK_KEY', + 'labelsEnabled': True, + 'redisHost': 'localhost', + 'redisPort': 6379, + 'redisDb': 0, + 'redisPassword': 'somePassword', + ## if the user access is not the default 'root' user, inlcude parameter below + 'redisUsername' : 'username', + 'featuresRefreshRate': 5, + 'segmentsRefreshRate' : 60, + 'metricsRefreshRate' : 60, + 'impressionsRefreshRate' : 60, +} +``` + + + +## Localhost mode + +A developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the Split SDK can be started in **localhost** mode (aka off-the-grid mode). In this mode, the SDK neither polls nor updates Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the features. To use the SDK in localhost mode, replace the SDK Key with "localhost", as shown in the example below: + +With this mode, you can instantiate the SDKS using one of the following methods: + +* JSON: Full support, for advanced cases or replicating an environment by pulling rules from the Split cloud (from version `9.4.0`). +* YAML: Supports dynamic configs, individual targets, and default rules (from version `8.0.0`). +* .split: Legacy option, only treatment result. + +### JSON + +Since version `9.4.0`, our SDK supports localhost mode by using the JSON format. This version allows the user to map feature flags and segment definitions in the same format as the APIs that receive the data. This mode needs the following extra configuration to be set: + +| **Name** | **Description** | **Type** | +| --- | --- | --- | +| splitFile | Indicates the path of the feature flag file location to read | string | +| segmentDirectory | Indicates the path where all the segment files are located | string | +| localhostRefreshEnabled | Flag to run synchronization refresh for feature flags and segments in localhost mode. | bool | + +#### splitFile + +The following splitFile is a JSON that represents a SplitChange: + + + +```python +class SplitChange(object): + """SplitChange class""" + + @prperty + def splits(self): + """return splits" + return self._splits + + @property + def since(self): + """return since epoch time""" + return self._since + + @property + def till(self) + """return till epoch time""" + return self._till +``` + + +```python +class Split(object): + """Split model object.""" + + @property + def name(self): + """Return name.""" + return self._name + + @property + def seed(self): + """Return seed.""" + return self._seed + + @property + def algo(self): + """Return hash algorithm.""" + return self._algo + + @property + def killed(self): + """Return whether the feature flag has been killed.""" + return self._killed + + @property + def default_treatment(self): + """Return the default treatment.""" + return self._default_treatment + + @property + def traffic_type_name(self): + """Return the traffic type of the feature flag.""" + return self._traffic_type_name + + @property + def status(self): + """Return the status of the feature flag.""" + return self._status + + @property + def change_number(self): + """Return the change number of the feature flag.""" + return self._change_number + + @property + def conditions(self): + """Return the condition list of the feature flag.""" + return self._conditions + + @property + def traffic_allocation(self): + """Return the traffic allocation percentage of the feature flag.""" + return self._traffic_allocation + + @property + def traffic_allocation_seed(self): + """Return the traffic allocation seed of the feature flag.""" + return self._traffic_allocation_seed +``` + + +```json +{ + "splits": [ + { + "trafficTypeName": "user", + "name": "feature_flag_1", + "trafficAllocation": 100, + "trafficAllocationSeed": -1364119282, + "seed": -605938843, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1660326991072, + "algo": 2, + "configurations": {}, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "segment_1" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + } + ], + "label": "in segment segment_1" + }, + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 50 + }, + { + "treatment": "off", + "size": 50 + } + ], + "label": "default rule" + } + ] + } + ], + "since": -1, + "till": 1660326991072 +} +``` + + + +#### segmentDirectory + +The provided segment directory must have the JSON files of the corresponding segment linked to previous feature flag definitions. According to the Feature flag file sample above: `feature_flag_1` has `segment_1` linked. That means that the segmentDirectory must have `segment_1` definition. + + + +```python +class SegmentChange(object): + """SegmentChange object class.""" + + @property + def name(self): + """Return segment name.""" + return self._name + + @property + def added(self): + """Return the segment keys to be added.""" + return self._added + + @property + def removed(self): + """Return the segment keys to be removed.""" + return self._removed + + @property + def since(self): + """return since epoch time""" + return self._since + + @property + def till(self) + """return till epoch time""" + return self._till +``` + + +```json +{ + "name": "segment_1", + "added": [ + "example1", + "example2" + ], + "removed": [], + "since": -1, + "till": 1585948850110 +} +``` + + + +**Init example** + + +```python +config = { + 'splitFile': 'parentRoot/splits.json', + 'segmentDirectory': '/parentRoot/segments', + 'localhostRefreshEnabled': True + } +factory = get_factory('localhost', config = config) + +try: + factory.block_until_ready(5) ## wait up to 5 seconds +except TimeoutException: + print("SDK TIMED OUT") + +``` + + +```python +from splitio import get_factory_async + +async def main(): + config = { + 'splitFile': 'parentRoot/splits.json', + 'segmentDirectory': '/parentRoot/segments', + 'localhostRefreshEnabled': True + } + factory = await get_factory_async('localhost', config = config) + + try: + await factory.block_until_ready(5) ## wait up to 5 seconds + except TimeoutException: + print("SDK TIMED OUT") + +loop = asyncio.new_event_loop() +loop.run_until_complete(main()) + +``` + + + +### YAML + +Since version 8.0.0, our SDK supports a new type of localhost feature flag definition file, using the YAML format. This new format allows the user to map different keys to different treatments within a single feature flag, and also add configurations to them. The new format is a list of single-key maps (one per mapping feature-flag-keys-config), defined as follows: + +```yaml title="YAML" +# - feature_flag_name: +# treatment: "treatment_applied_to_this_entry" +# keys: "single_key_or_list" +# config: "{\"desc\" : \"this applies only to ON treatment\"}" + +- my_feature_flag: + treatment: "on" + keys: "key" + config: "{\"desc\" : \"this applies only to ON treatment\"}" +- some_other_feature_flag: + treatment: "off" +- my_feature_flag: + treatment: "off" +``` + +In the example above, we have 3 entries: + * The first entry defines that for feature flag `my_feature_flag`, the key `key` will return the treatment `on` and the `on` treatment will be tied to the configuration `{"desc" : "this applies only to ON treatment"}`. + * The second entry defines that the feature flag `some_other_feature_flag` will always return the `off` treatment and no configuration. + * The third entry defines that `my_feature_flag` will always return `off` for all keys that don't match another entry (in this case, any key other than `key`). + +### .SPLIT file + + + +```python +from splitio import get_factory + +factory = get_factory('localhost') +split = factory.client() +``` + + +```python +from splitio import get_factory_async + +async def main(): + factory = await get_factory_async('localhost') + split = factory.client() + +loop = asyncio.new_event_loop() +loop.run_until_complete(main()) +``` + + + +In this mode, the SDK loads a mapping of Feature Flag name to treatment from a file at `$HOME/.split`. For a given feature flag, the treatment specified in the file is returned for every customer. Should you want to use another file, you just need to set the `splitFile` key in the configuration dictionary passed at instantiation time, to the full path of the desired file. + + +The following is a sample `.split` file. The format of this file is two columns separated by a whitespace. The left column is the feature flag name, and the right column is the treatment name. + +```bash title="Shell" +## sdk.get_treatment(*, reporting_v2) returns 'on' +reporting_v2 on + +double_writes_to_cassandra off + +new-navigation v3 +``` + +## Manager + +Use the Split Manager to get a list of feature flags available to the split client. + +To instantiate a Manager in your code base, use the same factory that you used for your client. + +```python title="Manager" +split = factory.client() +manager = factory.manager() +``` + +The Manager then has the following methods available. + + + +```python +class SplitManager(object): + def split_names(self): + """Returns the names of feature flags registered with the SDK. Subclasses need to override this method. + :return: A list of str + :rtype: list + """ + raise NotImplementedError() + + def splits(self): + """Retrieves the feature flags that are currently registered with the SDK. Subclasses need to override this method. + :return: A List of SplitView. + :rtype: list + """ + raise NotImplementedError() + + def split(self, feature_flag_name): + """Returns the Feature Flag registered with the SDK of this name. Subclasses need to override this method. + :return: The SplitView instance. + :rtype: SplitView + """ + raise NotImplementedError() +``` + + +```python +class SplitManagerAsync(object): + async def split_names(self): + """Returns the names of feature flags registered with the SDK. Subclasses need to override this method. + :return: A list of str + :rtype: list + """ + raise NotImplementedError() + + async def splits(self): + """Retrieves the feature flags that are currently registered with the SDK. Subclasses need to override this method. + :return: A List of SplitView. + :rtype: list + """ + raise NotImplementedError() + + async def split(self, feature_flag_name): + """Returns the Feature Flag registered with the SDK of this name. Subclasses need to override this method. + :return: The SplitView instance. + :rtype: SplitView + """ + raise NotImplementedError() +``` + + + +The `SplitView` object that you see referenced above has the following structure. + +```python title="SplitView" +SplitView = namedtuple('SplitView', ['name', 'traffic_type', 'killed', 'treatments', 'change_number', 'configs', 'default_treatment', 'sets', 'impressions_disabled']) +``` + +## Listener + +Split SDKs send impression data back to Split servers periodically when evaluating feature flags. To send this information to a location of your choice, define and attach an *impression listener*. Use the SDK's `impressionListener` parameter, where you can add an implementation of `ImpressionListener`. This implementation **must** define the `log_impression` method. It receives data in the following schema. + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| impression | impression | Impression object that has the feature name, treatment result, label, etc. | +| attributes | array | A list of attributes passed by the client. | +| instance-id | string | The IP address of the machine running the SDK. | +| sdk-language-version | string | The version of the SDK. In this case the language is `python` plus the version. | + +## Implement a custom impression listener + +Here is an example of how implement a custom impression listener. + + + +```python +# Import ImpressionListener interface +from splitio.impressions import ImpressionListener + +# Implementation Sample for a Custom Impression Listener +class CustomImpressionListener(ImpressionListener) +{ + def log_impression(self, data): + ## Custom behavior +} +``` + + +```python +# Import ImpressionListener interface +from splitio.impressions import ImpressionListener + +# Implementation Sample for a Custom Impression Listener +class CustomImpressionListener(ImpressionListener) +{ + async def log_impression(self, data): + # Custom behavior +} +``` + + + +## Attach a custom impression listener + +Here is an example of how to implement a custom impression listener. + + + +```python +from splitio import get_factory + +factory = get_factory( + 'YOUR_SDK_KEY', + config={ + ## ... + 'impressionListener': CustomImpressionListener() + }, + ## ... +) +split = factory.client() +``` + + +```python +from splitio import get_factory_async + +async def main(): + factory = await get_factory_async( + 'YOUR_SDK_KEY', + config={ + ## ... + 'impressionListener': CustomImpressionListener() + }, + ## ... + ) + split = factory.client() + +loop = asyncio.new_event_loop() +loop.run_until_complete(main()) +``` + + + +## Logging + +Since version `8.3.0` the loggers use a hierarchical approach, which enable the user to handle all split-sdk related logs either as a whole or as independent components. Each module has it's own logger, the root being `splitio`. +Below is an example of simple usage. + +```python title="Multi-threaded" +import logging +logger = logging.getLogger('splitio') +handler = logging.StreamHandler() +formatter = logging.Formatter('%(asctime)s %(name)-12s %(levelname)-8s %(message)s') +handler.setFormatter(formatter) +logger.addHandler(handler) +logger.setLevel(logging.WARNING) +``` + +For asyncio mode, since the SDK uses the same logger library, we suggest to create a custom listener to fetch logging lines in a separate thread to avoid any blocking in asyncio tasks during I/O logging operations, see example below: + +```python title="asyncio" +import asyncio +import logging +import logging.handlers +import time +from queue import SimpleQueue +from splitio import get_factory + +queue = SimpleQueue() +queue_handler = logging.handlers.QueueHandler(queue) +listener = logging.handlers.QueueListener( + queue, + logging.StreamHandler(), + logging.FileHandler('split.log'), +) +logger = logging.getLogger() +logFormatter = logging.Formatter('%(asctime)s %(name)-12s %(levelname)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S') +queue_handler.setFormatter(logFormatter) +logger.addHandler(queue_handler) +logger.setLevel('DEBUG') +logger.propagate = False +lhStdout = logger.handlers[0] +logger.removeHandler(lhStdout) + +listener.start() + +async def main(): + factory = get_factory('YOUR_SDK_KEY') + split = factory.client() + +try: + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) +finally: + listener.stop() +``` + +For older versions, to set a specific location or logging threshold, use the following syntax. + +```python title="Python" +import logging + +#Set logging configuration. +logging.basicConfig(filename='example.log',level=logging.WARNING) +``` + +## Proxy + +You can configure proxies by setting the environment variables `HTTP_PROXY` and `HTTPS_PROXY`. The SDK uses those variables to perform the server request. + +```python title="Example: Environment variables" +$ export HTTP_PROXY="http://10.10.1.10:3128" +$ export HTTPS_PROXY="http://10.10.1.10:1080" +``` diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/ruby-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/ruby-sdk.md new file mode 100644 index 00000000000..663fa07258d --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/ruby-sdk.md @@ -0,0 +1,595 @@ +--- +title: Ruby SDK +sidebar_label: Ruby SDK +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +This guide provides detailed information about our Ruby SDK. All of our SDKs are open source. Go to our [Ruby SDK GitHub repository](https://github.com/splitio/ruby-client) to learn more. + +## Initialization + +### 1. Import the SDK into your project + + + +```ruby +gem install splitclient-rb -v '~> 8.5.0' +``` + + +```ruby +gem install splitclient-rb -v '~> 8.5.0' +``` + + + +:::warning[If using Synchronizer with Redis - Synchronizer 2.x required after SDK Version 3.x] + +Since version 2.0.0 of the split-synchronizer, we use a more efficient scheme to store impressions in Redis. This approach is faster and easier on your Redis instances, since it yields better throughput of impressions to the backend. If you use this SDK with the Synchronizer in Redis or Proxy mode, you need the newest versions of our Split Synchronizer. We recommend that once you're using SDK versions compatible with Split-Sync 2.0 on all your applications pointing to the redis instance maintained by the Split-Sync, you disable backwards compatibility. This is as easy as changing a parameter to `true` on the JSON config or an environment variable to `on` if you're using the docker image. +::: + +### 2. Instantiate the SDK and create a new Split client + +When the SDK is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds, depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while its in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). + +To make sure the SDK is properly loaded before asking it for a treatment, block it until the SDK is ready. You can do this by using the `block_until_ready` method of the Split Client (or Manager) as part of the instantiation process of the SDK as shown below. Do this as a part of the startup sequence of your application. + +We recommend instantiating the Split factory once as a singleton and reusing it throughout your application. + +Configure the SDK with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split user interface, on your Admin settings page, API keys section. Select a server-side SDK API key. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. + +```ruby title="Ruby" +require 'splitclient-rb' + +split_factory = SplitIoClient::SplitFactory.new('YOUR_SDK_KEY') +split_client = split_factory.client + +begin + split_client.block_until_ready +rescue SplitIoClient::SDKBlockerTimeoutExpiredException + puts 'SDK is not ready. Decide whether to continue or abort execution' +end +``` + +### Configure the SDK for use with Rails + +Our SDK is compatible with Ruby on Rails. There are a few extra steps for the initialization. You can configure the SDK to work with Rails with the code snippet below. + +```ruby title="Ruby" +split_factory = SplitIoClient::SplitFactory.new('YOUR_SDK_KEY') +Rails.configuration.split_client = split_factory.client +``` + +To access the SDK client in your controllers, use the code snippet below: + +```ruby title="Ruby" +Rails.application.config.split_client +``` + +Now you can start asking the SDK to evaluate treatments for your customers. + +### SDK Server Compatibility + +The Split Ruby SDK has been tested as a standalone app using the following web servers: +* Puma +* Passenger +* Unicorn + +For other setups, contact [support@split.io](mailto:support@split.io). + +#### Unicorn and Puma in cluster mode + +**Note:** This is only applicable when using "memory storage". + +During the start of your application, the SDK spawns multiple threads. Each thread has an infinite loop inside, which is used to fetch feature flags/segments or send impressions/events to the Split service continuously. When using Unicorn or Puma in cluster mode (i.e. with `workers` > 0) the application server will spawn multiple child processes, but they won't recreate the threads that existed in the parent process. So, if your application is running in Unicorn or Puma in cluster mode you need to make two small extra steps. + +For both servers, you need to have the following line in your `config/initializers/splitclient.rb`: + +```ruby +Rails.configuration.split_factory = factory +``` + +Find below the specific setup for each one: + +#### Unicorn + +If you’re using Unicorn in cluster mode, you’ll need to include these lines in your Unicorn config (likely `config/unicorn.rb`): + +```ruby +before_fork do |server, worker| + ## keep your existing before_fork code if any + Rails.configuration.split_factory.stop! +end +after_fork do |server, worker| + ## keep your existing after_fork code if any + Rails.configuration.split_factory.resume! +end +``` + +#### Puma + +If using Puma in cluster mode, add these lines to your Puma config (likely `config/puma.rb`): + +```ruby +before_fork do + ## keep your existing before_fork code if any + Rails.configuration.split_factory.stop! +end +on_worker_boot do + ## keep your existing on_worker_boot code if any + Rails.configuration.split_factory.resume! +end +``` + +By doing the above, the SDK recreates the threads for each new worker and prevents the master process (that doesn't handle requests) from needlessly querying the Split service. + +:::danger[Server spawning method] +If you are running NGINX with `thread_spawn_method = 'smart'`, use our Redis integration with the [Split Synchronizer](https://help.split.io/hc/en-us/articles/360019686092-Split-synchronizer) or contact [support@split.io](mailto:support@split.io) for alternatives to run Split. +::: + +## Using the SDK + +### Basic use + +After you instantiate the SDK client, you can start using the `get_Treatment` method of the SDK client to decide what version of your features your customers are served. The method requires the `FEATURE_FLAG_NAME` attribute that you want to ask for a treatment and a unique `KEY` attribute that corresponds to the end user that you want to serve the feature to. + +From there, you simply need to use an if-else-if block as shown below and insert the code for the different treatments that you defined in the Split user interface. Remember the final else branch in your code to handle the client returning [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). + +```ruby title="Ruby" +## The key here represents the ID of the user, account, etc. you're trying to evaluate a treatment for +treatment = split_client.get_treatment('KEY', 'FEATURE_FLAG_NAME'); + +if treatment == 'on' + ## insert code here to show on treatment +elsif treatment == 'off' + ## insert code here to show off treatment +else + ## insert your control treatment code here +end +``` + +### Attribute syntax + +To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK's `get_treatment` method needs to be passed an attribute map at runtime. + +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `get_treatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in the Split user interface to decide whether to show the `on` or `off` treatment to this account. + +The `get_treatment` method supports five types of attributes: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: + +* **Strings:** Use type String. +* **Numbers:** Use type `long` or `int`. +* **Dates:** Express the value as `seconds since epoch` and as objects of class `DateTime`. +* **Booleans:** Use type Boolean. +* **Sets:** Use type `List`. + +```ruby title="Ruby" +attributes = {} +attributes[:deal_size] = 10000 +attributes[:registered_date] = Time.now.to_i ## any time as seconds since epoch +attributes[:plan_type] = "growth" +attributes[:permissions] = ['read', 'write'] +attributes[:paying_customer] = true + +treatment = split_client.get_treatment("KEY", + "FEATURE_FLAG_NAME", + attributes) + +if treatment == 'on' + ## insert on code here + elsif treatment == 'off' + ## insert off code here + else + ## insert control code here +end +``` + +### Multiple evaluations at once + +In some instances, you may want to evaluate treatments for multiple feature flag at once. Use the different variations of `get_treatments` method from the split client to do this. +* `get_treatments`': Pass a list of the feature flag names you want treatments for. +* `get_treatments_by_flag_set`: Evaluate all flags that are part of the provided set name and are cached on the SDK instance. +* `get_treatments_by_flag_sets`: Evaluate all flags that are part of the provided set names and are cached on the SDK instance. + + + +```ruby +attributes = {} +split_client.get_treatments('key', ['FEATURE_FLAG_NAME_1', 'FEATURE_FLAG_NAME_2'], attributes) +``` + + +```ruby +attributes = {} +treatments = split.get_treatments_by_flag_set('key', 'backend', attributes) +``` + + +```ruby +attributes = {} +treatments = split.get_treatments_by_flag_sets('key', ['backend', 'server_side'], attributes) +``` + + + +You can also use the [Split Manager](#manager) if you want to get all of your treatments at once. + +### Get Treatments with Configurations + +To [leverage dynamic configurations with your treatments](https://help.split.io/hc/en-us/articles/360026943552), you should use the `get_treatment_with_config` method. + +This method will return an object containing the treatment and associated configuration. + +The config element will be a stringified version of the configuration JSON defined in the Split user interface. If there are no configs defined for a treatment, the SDK returns `None` for the config parameter. + +This method takes the exact same set of arguments as the standard `get_treatment` method. See below for examples on proper usage: + +```ruby title="get_treatment_with_config" +result = client.get_treatment_with_config('key', 'new_boxes', attributes) +configs = JSON.parse(result[:config]) +treatment = result[:treatment] +``` + +If you need to get multiple evaluations at once, you can also use the `get_treatments_with_config` methods. +These methods take the exact same arguments as the [get_treatments](#multiple-evaluations-at-once) methods but return a mapping of feature flag names to SplitResult objects instead of strings. Example usage below. + + + + +```ruby + +feature_flag_names = ['FEATURE_FLAG_NAME_1', 'FEATURE_FLAG_NAME_2'] +feature_flag_results = client.get_treatments_with_config('KEY', feature_flag_names) + + ## feature_flag_results will have the following format: + ## { + ## 'FEATURE_FLAG_NAME_1': ('on', '{"color": "red"}'), + ## 'FEATURE_FLAG_NAME_2': ('v2', '{"copy": "better copy"}') + ## } +``` + + +```ruby +attributes = {} +result = split.get_treatments_with_config_by_flag_set('key', 'backend', attributes) +result.each do |feature_flag, treatment_with_config| + configs = JSON.parse(treatment_with_config[:config]) + treatment = treatment_with_config[:treatment] + puts "Feature: #{feature_flag}, Treatment: #{treatment}, Config: #{configs}" +end +``` + + +```ruby +attributes = {} +result = split.get_treatments_with_config_by_flag_sets('key', ['backend', 'server_side'], attributes) +result.each do |feature_flag, treatment_with_config| + configs = JSON.parse(treatment_with_config[:config]) + treatment = treatment_with_config[:treatment] + puts "Feature: #{feature_flag}, Treatment: #{treatment}, Config: #{configs}" +end +``` + + + +### Shutdown + +Call the `.destroy` method before letting a process using the SDK exit, as this method gracefully shuts down the Split SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. + +```ruby title="Ruby" +client.destroy +``` + +**Note: Within multi-threaded setups like using Ruby with Rails and Puma, to fully destroy the factory, you need to set it to nil as follows: `Rails.configuration.split_factory = nil`** + +:::warning[Important!] +A call to the `destroy()` method also destroys the factory object. When creating new client instance, first create a new factory instance. +::: + +## Track + +Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users’ actions and metrics. + +[Learn more](https://help.split.io/hc/en-us/articles/360020585772) about using track events in feature flags. + +In the examples below, you can see that the `.track()` method can take up to four arguments. The proper data type and syntax for each are: + +* **key:** The `key` variable used in the `get_treatment` call and firing this track event. The expected data type is **String**. +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: + * Contains 63 characters or fewer. + * Starts with a letter or number. + * Contains only letters, numbers, hyphen, underscore, or period. + * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` +* **VALUE:** (Optional) The value used in creating the metric. This field can be sent in as nil or 0 if you intend to only use the count function when creating a metric. The expected data type is **Integer** or **Float**. +* **PROPERTIES:** (Optional) A Hash of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. + +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK successfully queued the event to be sent back to Split's servers on the next event post. The SDK returns `false` if the current queue size is equal to the config set by `events_queue_size` or if an incorrect input to the `track` method is provided. + +In the case that a bad input has been provided, refer to the [Events](https://help.split.io/hc/en-us/articles/360020585772-Track-events) guide for more information about our SDK's expected behavior. + +```ruby title="Ruby" +## If you would like to send an event without a value +track_event = split_client.track('KEY', 'TRAFFIC_TYPE', 'EVENT_TYPE') + +## Example +track_event = split_client.track('john@doe.com', 'user', 'page_load_time') + +## If you would like to associate a value to an event +track_event = split_client.track('KEY', 'TRAFFIC_TYPE', 'EVENT_TYPE', VALUE) + +## Example +track_event = split_client.track('john@doe.com', 'user', 'page_load_time', 83.334) + +## If you would like to associate just properties to an event +track_event = split_client.track('KEY', 'TRAFFIC_TYPE', 'EVENT_TYPE', nil, { PROPERTIES }) + +## If you would like to associate a value and properties to an event +track_event = split_client.track('KEY', 'TRAFFIC_TYPE', 'EVENT_TYPE', VALUE, { PROPERTIES }) + +## Example +properties = { + package: 'premium', + admin: true, + discount: 50 +} + +track_event = split_client.track('john@doe.com', 'user', 'page_load_time', nil, properties) +``` + +## Configuration + +The SDK has a number of knobs for configuring performance. Each knob is tuned to a reasonable default. However, you can override the value while instantiating the SDK. The parameters available for configuration are described below: + +| **Configuration** | **Description** | **Default value** | +| --- | --- | --- | +| logger | The log implementation to use for warnings and errors from the SDK. | Logs to STDOUT | +| debug_enabled| Enabled verbose mode. | false | +| transport_debug_enabled | Super verbose mode that prints network payloads among others. | false | +| connection_timeout| HTTP client connection timeout (in seconds). | 5s | +| read_timeout | HTTP socket read timeout (in seconds). | 5s | +| features_refresh_rate |The SDK polls Split servers for changes to feature flags at this period (in seconds). | 5s | +| segments_refresh_rate | The SDK polls Split servers for changes to segments at this period (in seconds). | 60s | +| telemetry_refresh_rate | The SDK caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers (in seconds). | 3600s | +| impressions_refresh_rate | How often impressions are sent out (in seconds). | 60s | +| events_push_rate | How often events are sent out (in seconds). | 60s | +| cache_adapter| Where to store feature flags and impressions: `:memory` or `:redis` | `:memory` | +| redis_url | Redis URL or hash with configuration for SDK to connect to. See [http://www.rubydoc.info/github/redis/redis-rb/Redis%3Ainitialize](http://www.rubydoc.info/github/redis/redis-rb/Redis%3Ainitialize) | 'redis://127.0.0.1:6379/0' | +| mode | Whether the SDK is running in `standalone mode` using memory storage or `consumer mode` using an external storage. See Redis integration. | `:standalone` | +| redis_namespace | Prefix to add to elements in Redis cache when having to share Redis with other applications. | `"SPLITIO/ruby-#{VERSION}"` | +| labels_enabled | Disable labels from being sent to the Split backend. Labels may contain sensitive information. | true | +| impressions_queue_size | The size of the impressions queue in case of `cache_adapter == :memory`. | 5000 | +| events_queue_size | The size of the events queue in case of `cache_adapter == :memory`. | 500 | +| impressions_bulk_size | Max number of impressions to be sent to the backend on each post. | impressions_queue_size | +| ip_addresses_enabled | Flag to disable IP addresses and host name from being sent to the Split backend. | true | +| streaming_enabled | Boolean flag to enable the streaming service as default synchronization mechanism. In the event of an issue with streaming, the SDK will fallback to the polling mechanism. If false, the SDK will poll for changes as usual without attempting to use streaming. | true | +| impressions_mode | Defines how impressions are queued on the SDK. Supported modes are OPTIMIZED(`:optimized`), NONE(`:none`), and DEBUG(`:debug`). In OPTIMIZED mode, only unique impressions are queued and posted to Split; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, all impressions are queued and sent to Split; this is useful for validations. Use DEBUG mode when you want every impression to be logged in Split user inferface when trying to debug your SDK setup. This setting does not impact the impression listener which receives all generated impressions locally. | `:optimized` | +| flag_sets_filter | This setting allows the SDK to only synchronize the feature flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the SDK instance, bringing all the benefits from a reduced payload. | nil | +To set each of these parameters, use the syntax below: + +```ruby title="Ruby" +options = {connection_timeout: 10, + read_timeout: 5, + impressions_refresh_rate: 360, + logger: Logger.new('logfile.log')} + +split_factory = SplitIoClient::SplitFactory.new('YOUR_SDK_KEY', options) +split_client = split_factory.client +``` + +## Sharing state: Redis integration + +**Configuring this Redis integration section is optional for most setups. Read the information below to determine if it might be useful for your project.** + +By default, the Split client stores the state it needs to compute treatments (rollout plans, segments, and so on) in memory. As a result, it is easy to get set up with Split by instantiating a client and starting to use it. Configuring this Redis integration section is optional for most setups. + +This simplicity hides one important detail that is worth exploring. Because each Split client downloads and stores state separately, a change in a feature flag is picked up by every client on its own schedule. Thus, if a customer issues back-to-back requests that are served by two different machines behind a load balancer, the customer can see different treatments for the same feature flag because one Split client may not have picked up the latest change. This drift in clients is natural and usually ignorable as long as each client sets an aggressive value for `features_refresh_rate` and `segments_refresh_rate`. You can learn more about setting these rates in the [Configuration section](#configuration). + +However, if your application requires a total guarantee that Split clients across your entire infrastructure pick up a change in a feature flag at the exact same time, the only way to ensure that is to externalize the state of the Split client in a data store hosted on your infrastructure. + +We currently support Redis for this external data store. + +To use the Ruby SDK with Redis, set up the Split Synchronizer and instantiate the SDK in consumer mode. + +#### Split Synchronizer + +Follow the steps in our [Split Synchronizer](https://help.split.io/hc/en-us/articles/360019686092) document to get everything set to sync data to your Redis cache. After you do that, you can set up the SDK in consumer mode. + +#### Consumer Mode + +In consumer mode, a client can be embedded in your application code and respond to calls to `get_treatment` by retrieving state from the data store (Redis in this case). + +Here is how to configure and get treatments for a Split client in consumer mode. + +```ruby title="Ruby" +options = { + ## Other options here + cache_adapter: :redis, + mode: :consumer, + redis_url: 'redis://127.0.0.1:6379/0' +} + +split_factory = SplitIoClient::SplitFactory.new('YOUR_SDK_KEY', options) +split_client = split_factory.client +``` + +### Configure Redis using Sentinel + +Use the syntax below to configure Redis using Sentinel: + +```ruby title="Ruby" +SENTINELS = [{host: '127.0.0.1', port: 26380}, + {host: '127.0.0.1', port: 26381}] + +redis_connection = { + url: 'redis://mymaster', + sentinels: SENTINELS, + role: :master +} + +options = { + ## Other options here + redis_url: redis_connection +} + +split_factory = SplitIoClient::SplitFactory.new('YOUR_SDK_KEY', options) +split_client = split_factory.client +``` + +### Redis Cluster + +This functionality is currently not supported for this SDK, but is coming in a future release! Subscribe to our [release notes](https://www.split.io/releases) for updates. + +## Localhost mode + +Features start their life on one developer's machine. A developer should be able to put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the Split SDK can be started in **localhost** mode (aka off-the-grid mode). In this mode, the SDK neither polls nor updates Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the features. + +To use the SDK in localhost mode, replace the SDK Key with "localhost", as shown in the example below: + +```ruby title="Ruby" +require 'splitclient-rb' +split_file = File.expand_path(File.join(File.dirname(__FILE__), '../test_data/local_treatments/split.yaml')) + +split_client = split_factory = SplitIoClient::SplitFactory.new('localhost', split_file: split_file).client +``` + +In this mode, the SDK loads a `.yaml` file containing a simple depiction of a feature flag. eg: + +```yaml title="YAML" +- single_key_feature: + treatment: 'on' + keys: 'john_doe' + config: {'desc': 'this applies only to ON and only for john_doe. The rest will receive OFF'} +- single_key_feature: + treatment: 'off' + keys: + config: {'desc': 'this applies only to OFF treatment'} +``` + +In the example given, a call to `get_treatment` passing `john_doe` as the key renders the `on` treatment, while as any other key receives `off`. Note that configs can be added to test the `_with_config` versions of `get_treatment` and `get_treatments`. Also, you can set multiple keys for the same treatment using an array: + + +```yaml title="YAML" +- multiple_keys_feature: + treatment: 'on' + keys: ['john_doe', 'jane_doe'] + config: {'desc': 'this applies only to ON and only for john_doe and jane_doe. The rest will receive OFF'} +``` + +Any feature that is not provided in the `split_file` markup map returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment) if the SDK is asked to evaluate them. + +By default, changes in the file are not automatically picked up without restarting the client. To have the client automatically pick up changes to the file, specify `reload_rate` as the interval in seconds at which changes are picked up. Here is an example of specifying both `split_file` and `reload_rate`. + +```ruby title="Ruby" +factory = SplitIoClient::SplitFactoryBuilder.build('localhost', split_file: '/where/to-look-for/', reload_rate: 3) +``` + +## Manager + +Use the Split Manager to get a list of feature flags available to the Split client. To instantiate a Manager in your code base, use the same factory that you used for your client. + +```ruby title="Manager" +## Reusing the split_factory created originally. +split_manager = split_factory.manager +``` + +The `SplitView` object referenced above has the following structure. + +```ruby title="Manager" +## returns a List of SplitViews or empty. +list_of_feature_flags = split_manager.splits + +## Array of String representing feature flag names +list_of_feature_flag_names = split_manager.split_names + +## returns a SplitView of the 'name' specified or empty. +feature_flag = split_manager.split(name) +``` + +The `feature_flag` object referenced above has the following structure. + +```ruby title="Feature flags by Split Manager" +{ + :name=>"new_reporting", + :traffic_type_name=>"user", + :killed=>false, + :treatments=>["v1", "v2", "v3"], + :change_number=>1469134003507, + :configs=>{:on=>"{\"size\":15,\"test\":20}"}, + :default_treatment=>"off", + :sets>=["backend"], + :impressions_disabled=>false +} +``` + +## Listener + +Split SDKs send impression data back to Split servers periodically when evaluating feature flags. To send this information to a location of your choice, define and attach an *impression listener*. + +The SDK sends the generated impressions to the impression listener right away. However, to avoid blocking the caller thread, use the second parameter to specify the size of the queue acting as a buffer. Refer to the followoing snippet: + +If the impression listener is slow at processing the incoming data, the queue fills up and any subsequent impressions are dropped. + +```ruby title="Listener" +class MyImpressionListener + def log(impression) + Logger.new($stdout).info(impression) + end +end + +options = { + ## other options + impression_listener: MyImpressionListener.new ## do remember to initialize your class here + ## other options +} + +factory = SplitIoClient::SplitFactoryBuilder.build(sdk_key, options) +``` + +## Logging + +Our Ruby SDK makes use of Ruby’s stdlib `Logger` class to log errors/events. The default option is shown below: + +```ruby title="Ruby" +Logger.new($stdout) +``` + +You can configure the following options in the config file. + +```ruby title="Ruby" +{ + ## ... + ## you can specify your own Logger class instance here: + logger: Logger.new('logfile.log'), + ## to enable more verbose logging, including more debug information (false is the default) use: + debug_enabled: true, + ## to log transport data (mostly http requests, false is the default) use: + transport_debug_enabled: true + ## ... +} +``` + +## Proxy + +Ruby SDK respects the `HTTP_PROXY` environment variable. To use a proxy, assign a proxy address to that variable. + +```ruby title="Proxy" +http_proxy=http://username:password@hostname:port +``` + +## Troubleshooting + +I am seeing the following certificate error: `OpenSSL::SSL::SSLError` + +On OSX, if you see an SSL issue that looks similar to the example below, refer to [this post](https://toadle.me/2015/04/16/fixing-failing-ssl-verification-with-rvm.html) for troubleshooting. + +```ruby title="Error message" +OpenSSL::SSL::SSLError: SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed +``` \ No newline at end of file diff --git a/sidebars.ts b/sidebars.ts index c8ff29df02c..04ee0a58a09 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -920,6 +920,13 @@ const sidebars: SidebarsConfig = { dirName: "feature-management-experimentation/10-getting-started", }, //"feature-management-experimentation/sdks-and-infrastructure/index", + { + type: "category", + label: "SDKs and infrastructure", + collapsed: true, + items: [ { type: "autogenerated", dirName: "feature-management-experimentation/20-sdks-and-infrastructure", } + ], + }, "feature-management-experimentation/fme-support", ], }, From aebc5afb1894b40011025112060a7cd12ff13c08 Mon Sep 17 00:00:00 2001 From: lena sano Date: Tue, 4 Mar 2025 12:36:46 -0300 Subject: [PATCH 06/19] FME SDK Best practices and FAQs --- ...-features-into-a-serverless-environment.md | 167 ++++++++++++++++++ .../block-traffic-until-the-sdk-is-ready.md | 27 +++ ...hronizer-to-handle-high-impression-rate.md | 103 +++++++++++ .../frontend-and-backend-api-key-usage.md | 17 ++ .../moving-feature-flags-to-a-service.md | 138 +++++++++++++++ .../best-practices/split-sync-runbook.md | 1 + .../client-side-sdk-examples/android-app.md | 13 ++ .../android-kotlin.md | 13 ++ .../client-side-sdk-examples/ios-app.md | 13 ++ .../client-side-sdk-examples/ios-obj-c.md | 13 ++ .../ios-swift-app-two-factories.md | 13 ++ .../javascript-code.md | 13 ++ .../javascript-sdk-nextjs.md | 13 ++ .../client-side-sdk-examples/javascript.md | 15 ++ ...s-with-react-redux-using-javascript-sdk.md | 41 +++++ .../react-native-android-app.md | 13 ++ .../react-native-app-nodejs.md | 98 ++++++++++ .../react-native-ios-app.md | 13 ++ .../redux-sdk-running-on-client-side.md | 13 ++ ...sing-split-in-a-salesforce-lw-component.md | 13 ++ ...sing-split-with-multiple-web-components.md | 21 +++ ...ios-javascript-sdk-client-on-never-runs.md | 113 ++++++++++++ ...lient-destroy-does-not-post-impressions.md | 26 +++ ...-sdk-does-the-sdk-use-sharedpreferences.md | 17 ++ ...plicate-class-finalizablereferencequeue.md | 24 +++ ...-http-exception-chain-validation-failed.md | 26 +++ ...oid-sdk-sdk-takes-too-long-to-get-ready.md | 29 +++ ...in-sdk-always-returns-control-treatment.md | 38 ++++ .../browser-sdk-migration-guide.md | 136 ++++++++++++++ ...how-to-initialize-for-multiple-user-ids.md | 30 ++++ ...d-browser-sdk-does-the-sdk-cache-expire.md | 19 ++ ...changes-roll-out-slowly-to-user-devices.md | 30 ++++ .../ios-sdk-missing-track-method.md | 23 +++ ...-jfbcrypt-m-left-shift-of-x-by-y-places.md | 34 ++++ ...call-when-running-sdk-in-service-worker.md | 102 +++++++++++ ...sdk-does-sdk-ready-event-fire-only-once.md | 37 ++++ ...-not-supported-by-the-storage-mechanism.md | 39 ++++ ...ploy-javascript-sdk-to-a-wordpress-site.md | 66 +++++++ ...dk-how-to-enable-conent-security-policy.md | 34 ++++ ...st-mode-does-not-support-allowlist-keys.md | 51 ++++++ .../javascript-sdk-mysegments-endpoint.md | 38 ++++ ...t-sdk-not-ready-status-in-slow-networks.md | 54 ++++++ ...javascript-sdk-polimer-cli-enoent-error.md | 47 +++++ .../javascript-sdk-react-native.md | 35 ++++ ...act-sdk-error-building-app-with-webpack.md | 45 +++++ ...o-get-treatments-outside-the-components.md | 35 ++++ ...returning-true-when-react-sdk-times-out.md | 48 +++++ ...sdk-lazy-initialization-of-split-client.md | 80 +++++++++ ...atment-returned-when-sdk-is-initialized.md | 53 ++++++ .../always-getting-control-treatments.md | 41 +++++ ...that-does-not-exist-in-this-environment.md | 41 +++++ ...ow-do-i-find-out-what-changed-in-an-sdk.md | 21 +++ ...e-generated-impressions-and-events-load.md | 53 ++++++ .../how-to-use-split-sdks-with-split-proxy.md | 105 +++++++++++ ...ment-function-without-passing-a-user-id.md | 22 +++ ...alculate-a-treatment-for-a-feature-flag.md | 24 +++ .../isomorphic-javascript-wrapper-example.md | 24 +++ ...y-regardless-of-the-ready-timeout-value.md | 32 ++++ ...n-running-in-kubernetes-and-istio-proxy.md | 41 +++++ ...eturns-incomplete-list-of-feature-flags.md | 25 +++ ...hy-are-impressions-not-showing-in-split.md | 37 ++++ ...out-using-gettreatment-or-track-methods.md | 65 +++++++ .../faqs-optional-infra/_category_.json | 2 +- ...me-synchronizer-docker-container-in-aws.md | 124 +++++++++++++ ...synchronizer-docker-container-in-heroku.md | 147 +++++++++++++++ ...ficate-into-a-synchronizer-docker-image.md | 56 ++++++ .../http-error-when-using-proxy-mode.md | 35 ++++ .../no-impressions-sent-from-python-sdk-7.md | 17 ++ .../faqs-optional-infra/post-method-404.md | 49 +++++ ...running-evaluator-proxy-synchronizer-k8.md | 15 ++ .../sync-compatibility-matrix.md | 23 +++ .../using-sdk-sync-gettreatment-control.md | 55 ++++++ .../go-sdk-error-flushing-storage-queue.md | 34 ++++ ...sdk-exception-pkix-path-building-failed.md | 41 +++++ .../java-sdk-fatal-alert-handshake-failure.md | 34 ++++ .../java-sdk-how-to-change-log-level.md | 22 +++ .../java-sdk-how-to-deploy-in-aws-lambda.md | 101 +++++++++++ .../java-sdk-is-there-a-jar-file.md | 24 +++ ...t-error-nosuchmethoderror-google-common.md | 35 ++++ ...sdk-build-error-strongly-named-assembly.md | 31 ++++ .../net-xamarin-which-api-key.md | 21 +++ ...ncy-on-old-version-of-package-url-parse.md | 43 +++++ ..._modules-has-no-exported-member-splitio.md | 36 ++++ .../nodejs-sdk-how-to-deploy-in-aws-lambda.md | 82 +++++++++ ...alhost-mode-error-cannot-find-name-path.md | 42 +++++ ...oes-not-work-with-then-and-catch-blocks.md | 70 ++++++++ ...hp-unable-to-write-impressions-to-redis.md | 53 ++++++ ...dk-error-type-argument-1-must-be-string.md | 41 +++++ ...-sdk-close_wait-tcp-connections-in-puma.md | 34 ++++ .../ruby-sdk-error-uninitialized-constant.md | 29 +++ ...sing-sdk-with-rails-and-sidekiq-service.md | 31 ++++ .../ruby-sdk-upgrading-from-4-to-5-plus.md | 29 +++ .../optional-infra/split-daemon-splitd.md | 1 + .../optional-infra/split-evaluator.md | 1 + .../split-javascript-synchronizer-tools.md | 1 + .../optional-infra/split-proxy.md | 1 + .../optional-infra/split-synchronizer.md | 1 + .../server-side-sdk-examples/go-app.md | 13 ++ .../go-sdk-localhost-mode-yaml.md | 13 ++ .../server-side-sdk-examples/java-app.md | 13 ++ .../java-sdk-scala-sbt-cl.md | 13 ++ .../net-core-csharp-app.md | 13 ++ .../server-side-sdk-examples/net-core-vb.md | 79 +++++++++ .../server-side-sdk-examples/net-csharp.md | 13 ++ .../net-sdk-debug-logging.md | 13 ++ .../nodejs-vue-nuxt-ssr.md | 13 ++ .../server-side-sdk-examples/php-app.md | 13 ++ .../server-side-sdk-examples/python-app.md | 13 ++ .../python-django-uwsgi.md | 46 +++++ .../server-side-sdk-examples/ruby-app.md | 13 ++ .../ruby-on-rails-puma.md | 13 ++ .../ruby-sdk-rails-caching.md | 69 ++++++++ 112 files changed, 4321 insertions(+), 1 deletion(-) create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/best-practices-for-integrating-fme-features-into-a-serverless-environment.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/block-traffic-until-the-sdk-is-ready.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/configure-fme-synchronizer-to-handle-high-impression-rate.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/frontend-and-backend-api-key-usage.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/moving-feature-flags-to-a-service.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/android-app.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/android-kotlin.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-app.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-obj-c.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-swift-app-two-factories.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/javascript-code.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/javascript-sdk-nextjs.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/javascript.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/nodejs-with-react-redux-using-javascript-sdk.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-android-app.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-app-nodejs.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-ios-app.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/redux-sdk-running-on-client-side.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/using-split-in-a-salesforce-lw-component.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/using-split-with-multiple-web-components.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-ios-javascript-sdk-client-on-never-runs.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-calling-client-destroy-does-not-post-impressions.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-does-the-sdk-use-sharedpreferences.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-duplicate-class-finalizablereferencequeue.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-http-exception-chain-validation-failed.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-sdk-takes-too-long-to-get-ready.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-using-kotlin-sdk-always-returns-control-treatment.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/browser-sdk-migration-guide.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-and-android-sdk-how-to-initialize-for-multiple-user-ids.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-android-browser-sdk-does-the-sdk-cache-expire.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-android-browser-sdk-split-changes-roll-out-slowly-to-user-devices.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-sdk-missing-track-method.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-sdk-runtime-error-jfbcrypt-m-left-shift-of-x-by-y-places.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-cors-error-in-streaming-call-when-running-sdk-in-service-worker.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-does-sdk-ready-event-fire-only-once.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-error-shared-client-not-supported-by-the-storage-mechanism.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-how-to-deploy-javascript-sdk-to-a-wordpress-site.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-how-to-enable-conent-security-policy.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-localhost-mode-does-not-support-allowlist-keys.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-mysegments-endpoint.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-not-ready-status-in-slow-networks.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-polimer-cli-enoent-error.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-react-native.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-error-building-app-with-webpack.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-is-it-possible-to-get-treatments-outside-the-components.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-istimeout-prop-not-returning-true-when-react-sdk-times-out.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-lazy-initialization-of-split-client.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/redux-sdk-control-treatment-returned-when-sdk-is-initialized.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/always-getting-control-treatments.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/error-gettreatment-you-passed-split-name-that-does-not-exist-in-this-environment.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/how-do-i-find-out-what-changed-in-an-sdk.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/how-to-ensure-sdk-is-configured-to-handle-the-generated-impressions-and-events-load.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/how-to-use-split-sdks-with-split-proxy.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/is-it-possible-to-call-getthreatment-function-without-passing-a-user-id.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/is-it-possible-to-use-postman-to-calculate-a-treatment-for-a-feature-flag.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/isomorphic-javascript-wrapper-example.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/sdk-never-gets-ready-regardless-of-the-ready-timeout-value.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/sdk-readiness-always-times-out-when-running-in-kubernetes-and-istio-proxy.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/split-manager-returns-incomplete-list-of-feature-flags.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/why-are-impressions-not-showing-in-split.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/why-is-the-sdk-making-hundreds-of-network-calls-without-using-gettreatment-or-track-methods.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/how-to-deploy-fme-synchronizer-docker-container-in-aws.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/how-to-deploy-synchronizer-docker-container-in-heroku.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/how-to-inject-a-certificate-into-a-synchronizer-docker-image.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/http-error-when-using-proxy-mode.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/no-impressions-sent-from-python-sdk-7.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/post-method-404.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/running-evaluator-proxy-synchronizer-k8.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/sync-compatibility-matrix.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/using-sdk-sync-gettreatment-control.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/go-sdk-error-flushing-storage-queue.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-exception-pkix-path-building-failed.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-fatal-alert-handshake-failure.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-how-to-change-log-level.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-how-to-deploy-in-aws-lambda.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-is-there-a-jar-file.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-time-out-error-nosuchmethoderror-google-common.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/net-sdk-build-error-strongly-named-assembly.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/net-xamarin-which-api-key.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-dependency-on-old-version-of-package-url-parse.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-error-node_modules-has-no-exported-member-splitio.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-how-to-deploy-in-aws-lambda.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-localhost-mode-error-cannot-find-name-path.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-using-gettreatment-in-localhost-mode-does-not-work-with-then-and-catch-blocks.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/php-sdk-why-is-php-unable-to-write-impressions-to-redis.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/python-sdk-error-type-argument-1-must-be-string.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-close_wait-tcp-connections-in-puma.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-error-uninitialized-constant.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-example-using-sdk-with-rails-and-sidekiq-service.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-upgrading-from-4-to-5-plus.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/go-app.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/go-sdk-localhost-mode-yaml.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/java-app.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/java-sdk-scala-sbt-cl.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/net-core-csharp-app.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/net-core-vb.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/net-csharp.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/net-sdk-debug-logging.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/nodejs-vue-nuxt-ssr.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/php-app.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/python-app.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/python-django-uwsgi.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/ruby-app.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/ruby-on-rails-puma.md create mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/ruby-sdk-rails-caching.md diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/best-practices-for-integrating-fme-features-into-a-serverless-environment.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/best-practices-for-integrating-fme-features-into-a-serverless-environment.md new file mode 100644 index 00000000000..abd5c8f157d --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/best-practices-for-integrating-fme-features-into-a-serverless-environment.md @@ -0,0 +1,167 @@ +--- +title: Best practices for integrating Split feature flags into a serverless environment +sidebar_label: Best practices for integrating Split feature flags into a serverless environment +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 1 +--- + +

+ +

+ +In serverless environments, data persistence is best handled by externalizing state. This avoids the performance hit of "cold starts" where processes have to load and cache data before they can perform. This is the case with feature flagging SDKs as well. + +Read the blog post, ___Serverless Applications Powered by FME Feature Flags___ (below) for examples of how to achieve the best performance when using Split from within Lambda functions in Amazon AWS. + +## Serverless Applications Powered by FME Feature Flags + +The concept of [Serverless Computing](https://en.wikipedia.org/wiki/Serverless_computing), also called [Functions as a Service (FaaS)](https://martinfowler.com/articles/serverless.html#unpacking-faas) is fast becoming a trend in software development. This blog post will highlight steps and best practices for integrating Split feature flags into a serverless environment. + +### A quick look into Serverless Architecture + +[Serverless architectures](https://martinfowler.com/articles/serverless.html) enable you to add custom logic to other provider services, or to break up your system (or just a part of it) into a set of event-driven stateless functions that will execute on a certain trigger, perform some processing, and act on the result — either sending it to the next function in the pipeline, or by returning it as result of a request, or by storing it in a database. One interesting use case for FaaS is image processing where there is a need to validate the data before storing it in a database, retrieving assets from an S3 bucket, etc. + +Some advantages of this architecture include: + +* **Lower costs:** Pay only for what you run and eliminate paying for idle servers. With the pay-per-use model server costs will be proportional to the time required to execute only on requests made. +* **Low maintenance:** The infrastructure provider takes care of everything required to run and scale the code on demand and with high availability, eliminating the need to pre-plan and pre-provision servers servers to host these functions. +* **Easier to deploy:** Just upload new function code and configure a trigger to have it up and running. +* **Faster prototyping:** Using third party API’s for authentication, social, tracking, etc. minimizes time spent, resulting in an up-and-running prototype within just minutes. + +Some of the main providers for serverless architecture include, Amazon: [AWS Lambda](https://aws.amazon.com/lambda/); Google: [Cloud Functions](https://cloud.google.com/functions/); and Microsoft: [Azure Functions](https://azure.microsoft.com/en-us/services/functions/). Regardless of which provider you may choose, you will still reap the benefits of feature flagging without real servers. + +In this blog post, we’ll focus on AWS lambda with functions written in JavaScript running on [Node.js](https://help.split.io/hc/en-us/articles/360020564931-Node-js-SDK). Additionally we’ll highlight one approach to interacting with Split feature flags on a serverless application. It’s worth noting that there are several ways in which one can interact with Split on a serverless application, but we will highlight just one of them in this post. + +### Externalizing state + +If we are using Lambda functions in Amazon AWS, the best approach would be to use ElastiCache (Redis flavor) as an in-memory external data store, where we can store our feature rules that will be used by the Split SDKs running on Lambda functions to generate the feature flags. + +One way to achieve this is to set up the [Split Synchronizer](https://help.split.io/hc/en-us/articles/360019686092-Split-synchronizer), a background service created to synchronize Split information for multiple SDKs onto an external cache, Redis. To learn more about Split Synchronizer, check out our recent blog post. + +On the other hand, the Split Node SDK has a [built-in Redis integration](https://help.split.io/hc/en-us/articles/360020564931-Node-js-SDK#state-sharing-redis-integration) that can be used to communicate with a Redis ElastiCache cluster. The diagram below illustrates the set up: + +![](https://www.split.io/wp-content/uploads/split-redis-elasticache-architecture.jpg) +Redis holds Split definitions which are kept in sync by the Split Synchronizer and used by lambda functions​ + +### Step 1: Preparing the ElastiCache cluster + +Start by going to the [ElastiCache console](https://console.aws.amazon.com/elasticache/) and create a cluster within the same VPC that you’ll be running the Lambda functions from. Make sure to select Redis as the engine: + +![](https://www.split.io/wp-content/uploads/create-redis-elasticache-cluster.png) +Selecting Redis as the ElastiCache engine + +### Step 2: Run Split Synchronizer as a Docker Container Using ECS +The next step would be to deploy the Split Synchronizer on ECS (in [synchronizer mode](https://help.split.io/hc/en-us/articles/360019686092-Split-synchronizer)) using the existing [Split Synchronizer Docker image](https://hub.docker.com/r/splitsoftware/split-synchronizer/). Refer to this guide on how to [deploy docker containers](https://aws.amazon.com/getting-started/tutorials/deploy-docker-containers/). + +Now from the [EC2 Container Service (ECS) console](https://console.aws.amazon.com/ecs/) create an ECS cluster within the same VPC as before. As a next step create the task definition that will be used on the service by going to the Task Definitions page. This is where docker image repository will be specified, including any environment variables that will be required. + +As images on Docker Hub are available by default, specify the organization/image: + +![](https://www.split.io/wp-content/uploads/configure-ecs-task-split.png) +Configuring an ECS task to be a split-synchronizer docker image + +And environment variables (specifics can be found on the Split Synchronizer docs): + +![](https://www.split.io/wp-content/uploads/Updated-Screenshot.png) +Passing configuration keys to the Docker image as environment variables +Any Docker port mapping needed can be specified during the task creation. + +At this point we have the EC2 cluster and we have our task. The next step is to create a service that uses this task — go to your new cluster and click “create” on the services tab. You need to at least select the task and the number of tasks running concurrently: + +![](https://www.split.io/wp-content/uploads/create-split-ecs-service.png) +split-synchronizer ECS service creation +Finish with any custom configuration you may need, review and create the service. This will launch as many instances as specified. If there were no errors, the feature flags definitions provided by the Split service should already be in the external cache, and ready to be used by the SDKs integrated in the lambda functions that we’ll set up in the next section. + +### Step 3: Using Feature Flags on Lambda Functions + +There are two things we need to know before we start: + +1. The [Lambda programming model](http://docs.aws.amazon.com/lambda/latest/dg/programming-model-v2.html) uses a handler function, which Lambda will call when the function is triggered. It’s also the one that receives parameters from AWS: + * the event that triggered the function; + * the context; + * a callback function to return the results. +2. Lambda functions can be written directly on the AWS console as long as it doesn’t have any library dependencies. If extra dependencies are needed, a [deployment package](http://docs.aws.amazon.com/lambda/latest/dg/nodejs-create-deployment-pkg.html) should be built, which is no more than a .zip file with the functions code, as well as the required dependencies. Since we’ll be integrating a Split SDK to provide feature flags, we will be adding extra dependencies, and as such will have to create a deployment package. + +### Our custom code + +On the custom function, install the `@splitsoftware/splitio` npm package and include the node_modules folder on the zip. + +Step-by-step of an example function: + +1. Go to the working directory of the project and install the `@splitsoftware/splitio` package. +2. Create an `index.js` file. Require the `@splitsoftware/splitio` package there. +3. Instantiate the SDK client on [consumer mode](https://help.split.io/hc/en-us/articles/360020564931-Node-js-SDK#state-sharing-redis-integration) making sure it points to the correct Redis cluster (we’ll use an env variable for this in the next step). Don’t set a prefix for storage configurations unless same prefix was used for the Synchronizer. +4. Write whatever code is required but export the function as `handler`. + +One important thing to note — as async storage is used, async calls to the API will be received. + +View the example code below: + +```javascript +const splitio = require("@splitsoftware/splitio").SplitFactory; + + const handler = (event, context, callback) => { + const factory = splitio({ + mode: "consumer", + core: { + authorizationKey: "YOUR_AUTH_KEY", + }, + storage: { + type: "REDIS", + options: { + url: `redis://${process.env.ELASTICACHE_PRIMARY_ENDPOINT}/0`, + }, + }, + }); + + const client = factory.client(); + + await client.ready(); + + client + .getTreatment("a_key", "my_feature") + .then((treatment) => { + if (treatment == "on") { + // do something + } else { + // do something else + } + client.destroy(); + callback(null, `Treatment for a_key: ${treatment}`); + }) + // If we had an error, return it as the first argument in the callback + .catch((err) => callback(err)); + }; + + module.exports = handler; +``` + +Once the code has been written, it’s time to prepare the deployment package by creating a zip that includes `index.js` and the `node_modules` folder. Next, go to the [Lambda console](https://console.aws.amazon.com/lambda/) and select “create function”. On the blueprint selection page, select “Author from scratch” option and dd the trigger that will be used. It’s recommended not to enable it until you’re certain that the function works as expected. + +### Upload the code + +On the Lambda function code section, select the “Upload a .ZIP file” option. It can also be uploaded to S3 and the URL specified. Any environment variables required on Lambda can specified here (for example the one pointing to Redis ElastiCache needed in the previous step): + +![](https://www.split.io/wp-content/uploads/upload-split-lambda-code.png) +Uploading user code that runs as lambda functions + +Set up your handler function in the section called “Lambda function handler and role”. Leave the default as index.handler. + +Note that the first part is the file name inside the zip where the handler function is exported, and the second part is the function name. For example, if a file is called `app.js` and the function is called `myHandler`, the “Handler” value would be `app.myHandler`. + +On the Advanced settings of this step, set the VPC where the ElastiCache cluster is. + +Once the roles and anything else that is required has been configured, click next, review and create the function. + +That’s it! To test your function manually, just click the “Test” button, select a the synthetic trigger of preference and check that it works as expected. + +### Summary + +There are few ways to make use of Split [Feature Flags](https://www.split.io/articles/top-10-feature-flag-examples/) in a serverless application. This blog post covers the case of using Split Synchronizer and for javascript functions. + +In future posts we’ll share another approach using Split “callhome” or Split Evaluator which is a microservice that can evaluate flags and return the result, in addition to storing the rules to evaluate the flags as highlighted in this post. + +In case you’re wondering “can’t I hit the Split servers from my Lambda function?” The answer is yes, in a “standalone” mode, but it won’t be as efficient as having the state in one common place i.e. Redis. It’s NOT recommended to run the SDK in a standalone mode due to the latency it may incur at the creation of one SDK object per function. + +For further help using Split synchronizer in a serverless environment [contact us](https://www.split.io/company/contact/) or use the support widget in our cloud console — we’re here to help! \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/block-traffic-until-the-sdk-is-ready.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/block-traffic-until-the-sdk-is-ready.md new file mode 100644 index 00000000000..2aa760837a3 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/block-traffic-until-the-sdk-is-ready.md @@ -0,0 +1,27 @@ +--- +title: Block traffic until the SDK is ready +sidebar_label: Block traffic until the SDK is ready +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 5 +--- + +

+ +

+ +## Question + +When the SDK is instantiated, it kicks off background tasks to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds, depending on the size of data. While the SDK is in this intermediate state, if it is asked to evaluate which treatment to show to a customer for a specific feature flag, it may not have data necessary to run the evaluation. In this circumstance, the SDK does not fail, rather it returns the Control treatment. How can I avoid this? + +## Answer + +You can wait to send traffic by blocking until the SDK is ready. This is best done as part of the startup sequence of your application. Here is an example in Ruby: + +``` +require 'splitclient-rb'options = {block_until_ready:10 } +begin split_factory = SplitIoclient::SplitFactoryBuilder.build("YOUR_API_KEY", options) split_client = split_factory.client +rescue SplitIoClient::SDKBlockerTimeoutExpiredException + puts "SDK Failed to initialize in the time requested" +end +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/configure-fme-synchronizer-to-handle-high-impression-rate.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/configure-fme-synchronizer-to-handle-high-impression-rate.md new file mode 100644 index 00000000000..432bdbac2a8 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/configure-fme-synchronizer-to-handle-high-impression-rate.md @@ -0,0 +1,103 @@ +--- +title: Configure Split Synchronizer to handle high impression rate +sidebar_label: Configure Split Synchronizer to handle high impression rate +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 6 +--- + +

+ +

+ +## Question + +When using a server side Split SDK with the Split Synchronizer and Redis, what is the best configuration for Synchronizer to handle a high load of incoming impressions? + +## Answer + +The Split Synchronizer (version 1.6.0 and above) has configuration parameters that can be set to achieve this. + +The Synchronizer documentation shows the [configuration parameter details](https://docs.split.io/docs/split-synchronizer#section-advanced-configuration). The specific parameters that control the performance are: +* impressionsMaxSize +* impressionsRefreshRate +* impressionsThreads +* impressionsPerPost + +The impressionsMaxSize and impressionsPerPost parameters are configured according to the expected rate of incoming impressions. Since the Synchronizer is a multi-threaded process, increasing impressionsThreads can help shorten the time it takes to post impressions. + +Below is a full configuration setting for Synchronizer that can post 100,000 impressions per minute. + +Please make sure to update the JSON with the relevant API Key, Redis host, port and database number before applying it. + +``` +{ + "apiKey": "YOUR_API_KEY", + "proxy": { + "port": 3000, + "adminPort": 3010, + "adminUsername": "", + "adminPassword": "", + "dashboardTitle": "", + "persistInFilePath": "", + "impressionsMaxSize": 10485760, + "eventsMaxSize": 10485760, + "auth": { + "sdkAPIKeys": [ + "SDK_API_KEY" + ] + } + }, + "redis": { + "host": "localhost", + "port": 6379, + "db": 0, + "password": "", + "prefix": "", + "network": "tcp", + "maxRetries": 0, + "dialTimeout": 5, + "readTimeout": 10, + "writeTimeout": 5, + "poolSize": 10, + "sentinelReplication": false, + "sentinelAddresses": "", + "sentinelMaster": "" + }, + "sync": { + "admin": { + "adminPort": 3010, + "adminUsername": "", + "adminPassword": "", + "dashboardTitle": "" + } + }, + "log": { + "verbose": false, + "debug": false, + "stdout": false, + "file": "/tmp/split-agent.log", + "fileMaxSizeBytes": 2000000, + "fileBackupCount": 3, + "slackChannel": "", + "slackWebhookURL": "" + }, + "impressionListener": { + "endpoint": "" + }, + "splitsRefreshRate": 60, + "segmentsRefreshRate": 60, + "impressionsRefreshRate": 20, + "impressionsPerPost": 10000, + "impressionsThreads": 5, + "eventsPushRate": 60, + "eventsConsumerReadSize": 10000, + "eventsConsumerThreads": 1, + "metricsRefreshRate": 60, + "httpTimeout": 60 +} +``` + +##See also + +For more information on setup and configuration, see [Split Synchronizer Runbook](https://help.split.io/hc/en-us/articles/360018343391-Split-Synchronizer-Runbook). \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/frontend-and-backend-api-key-usage.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/frontend-and-backend-api-key-usage.md new file mode 100644 index 00000000000..ad33703d557 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/frontend-and-backend-api-key-usage.md @@ -0,0 +1,17 @@ +--- +title: Frontend and backend API key usage +sidebar_label: Frontend and backend API key usage +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 4 +--- + +

+ +

+ +As a practical matter, you need only a single front end and single back end API key for each Split environment. When an environment is created, Split automatically creates one key of each type for the new environment. + +There is nothing wrong with having multiple keys of the same type for the same environment, but there is no real reason to do so because Split does not currently track which API key is used. + +A client-side SDK (JavaScript, iOS, Android) should be initialized with a client-side API key. A server-side SDK (Go, Java, .NET, etc.) should be initialized with a server-side API key. The main difference between the access provided to client-side SDKs using a client-side API key and server-side SDKs using a server-side API key is the way they retrieve information about segments. The client-side SDKs hit the endpoint /memberships, which only returns the segments containing the ID used to initialize the SDK. The server-side SDKs call /segmentChanges, which downloads the entire contents of every segment in the environment. This way, the server-side SDKs can compute treatments for any possible ID, while the client-side SDKs minimize space overhead for browsers and mobile devices by downloading only the segment information needed to process getTreatment calls for the ID specified during initialization. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/moving-feature-flags-to-a-service.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/moving-feature-flags-to-a-service.md new file mode 100644 index 00000000000..ee8d4227b6a --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/moving-feature-flags-to-a-service.md @@ -0,0 +1,138 @@ +--- +title: Moving Feature Flags to a Service +sidebar_label: Moving Feature Flags to a Service +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 2 +--- + +

+ +

+ + +## Using a Service for Feature flags + +Split lets you roll out features and experiment with a target group of customers across the full web stack: from deep in the backend to client-facing Javascript and mobile. + +Feature flagging in mobile can be particularly advantageous. For example, consider what happens when a critical bug appears in a newly-released mobile feature: due to App Store approval delay, a fix can’t be delivered to customers in minutes; not to mention, you can't force customers to update their apps. + +Many mobile and IoT apps are highly optimized for the resource-constrained environment of the device. A feature flagging solution should have minimal or no significant impact on the size or performance of your mobile app. + +A customer should be able to access a new feature whether using the web, mobile, or IoT app. A lack of consistency in customer experience can lead to customer frustration. + +Split provides per-language libraries (.Net, Java, Node, PHP, Python, Ruby, Go) on the back end and iOS and Android libraries for mobile. For web, we provide a JavaScript SDK, as well as first-class support for React and Redux vía two libraries. When we do not support a language, we recommend wrapping one of our server-side libraries in a small service - hosted on your infrastructure. + +This same approach can provide benefits for your browser, mobile or IoT apps as well, querying the service at startup for which features are to be turned on and which are to be turned off. Let’s call it the 'phone home' approach. + +This approach has a number of advantages: + +* **Uniform experience across devices, versions and viewports** + It ensures a uniformity of experience across clients. Since the web app can also query the same service, our customers can be confident that their customers will either see a feature on or off, regardless of how they access the product - via mobile or web. Thus, phoning home is portable. + +* **Update your platform without touching the app** + Split is a core piece of our customers’ infrastructure and is always improving. By hosting Split on the server-side, our customers can confidently upgrade their server-side Split library, without having to worry about older, possibly conflicting, versions of Split being used in older mobile or IoT apps. Phoning home avoids versioning headaches. + +* **Use more data than available on the device or in the browser** + When deciding whether a user sees a feature or not, you often need to leverage user data that may not be available on the device. Take, for instance, demographic data about the user or outputs of data models. Instead of downloading such sensitive data to the front end, you can simply pass the data to Split on the server-side, thus bypassing the need to download and retain information on the browser or device. + +* **No impact to file size** + By hosting the library on the server side, you need never worry about increasing the footprint of your mobile or IoT app by adding Split’s library. Phoning home is safe. + +## Best Practices for Designing the Service + +Split provides the [Evaluator](https://help.split.io/hc/en-us/articles/360020037072-Split-Evaluator) as an out of the box solution for evaluating feature flags on the server-side, to both address potential client-side challenges and to split on applications written in languages for which there is no SDK. + +The app should call home and retrieve a mapping of features to the treatment (aka variation, experience) to show the app user for those features. Assuming a service deployed at /splits, we recommend using the following REST API: + +``` +@GET +/splits/{customer_id}?dimension_1={dimension_1_value}&dimension_2={dimension2_value}.... +``` + +Example: + +``` +/splits/4915?connection_speed=3G&country=usa&device_type=android.... +``` + +`customer_id`: a unique identifier for your customer. If your product has a web presence, this id should preferably be shared between web and mobile so that the customer will see the same treatment for a feature, whether they access it from the web or mobile app. + +`dimension_n_value`: The mobile app can send any dimensions about the customer that the server should take into account while determining which treatment to show to that customer. For instance: current location, connection speed or device type of the customer. + +Since dimension values are encoded in the query parameters, we recommend communicating over https. + +### Response Schema and Status Code + +The response object should follow this schema: + +``` +// a map of feature name to treatment +[ + { + 'featureName': string, + 'treatment' : string + } +] +``` + +The response status should be: + +`200` - this REST endpoint will always return a 200. In the case of failure, the 200 status code should be accompanied by an empty list as the response object. + +The app should assume that if a feature was not returned in the list, then the `control` treatment should be shown. `control` is a reserved treatment that indicates a problem in computing a treatment. When using Split, a developer should always handle control. For instance, here is sample Java code for a basic on/off feature: + +``` +String treatment = … // retrieved from the map returned by server +if ("on".equals(treatment)) { + // feature is 'on'. Turn it on for customer. +} else if ("off".equals(treatment)) { + // feature is 'off'. Turn it off for customer. +} else { + // feature is in 'control'. Handle it either as + // 'on' or 'off' depending on your use case or + // provide any special handling. +} +``` + +If ‘control’ means ‘off’, we can simplify this sample code to: + +``` +if ("on".equals(treatment)) { + // feature is 'on'. Turn it on for customer. +} else { + // feature is 'off'. Turn it off for customer. +} +``` + +### Server Code Sample + +Here is a Guice enabled Java pseudo-code for the REST server: + +``` +@Path("/splits") +public class SplitServer { + private SplitManager _manager; + private SplitClient _client; + @Inject + public SplitServer(SplitFactory factory) { + _manager = factory.manager(); + _client = factory.client(); + } + @Path("{customer_id}") + @GET + public Response evaluateAllFeatures(@PathParam("customer_id") id, + @Context UriInfo uriInfo) { + Map attributes = uriInfo.getQueryParams(); + List> result = new ArrayList(); + for (Split split : _manager.splits()) { + String t = _client.getTreatment(id, split.name(), attributes); + Map m = new HashMap(); + m.put("featureName", split.name()); + m.put("treatment", t); + result.add(m); + } + return Response.ok(result); + } +} +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/split-sync-runbook.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/split-sync-runbook.md index 8eb1f466ae5..5f9ba8bfe00 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/split-sync-runbook.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/split-sync-runbook.md @@ -3,6 +3,7 @@ title: Split Synchronizer runbook sidebar_label: Split Synchronizer runbook helpdocs_is_private: false helpdocs_is_published: true +sidebar_position: 3 --- diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/android-app.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/android-app.md new file mode 100644 index 00000000000..34c57edefc4 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/android-app.md @@ -0,0 +1,13 @@ +--- +title: Android App Project using Split SDK example +sidebar_label: Android App Project using Split SDK example +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 13 +--- + +

+ +

+ +[Android App Project using Split SDK Example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/android-sdk) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/android-kotlin.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/android-kotlin.md new file mode 100644 index 00000000000..bd4437ae806 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/android-kotlin.md @@ -0,0 +1,13 @@ +--- +title: Android Kotlin App Project using Split SDK example +sidebar_label: Android Kotlin App Project using Split SDK example +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 7 +--- + +

+ +

+ +[Android Kotlin App project using Split SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/Android-Kotlin-Split-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-app.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-app.md new file mode 100644 index 00000000000..d5f0195d529 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-app.md @@ -0,0 +1,13 @@ +--- +title: iOS App Project using Split SDK example +sidebar_label: iOS App Project using Split SDK example +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 14 +--- + +

+ +

+ +[iOS App Project using Split SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/iOS-Swift-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-obj-c.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-obj-c.md new file mode 100644 index 00000000000..3d55ab3e097 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-obj-c.md @@ -0,0 +1,13 @@ +--- +title: iOS Objective-C Project using Split SDK example +sidebar_label: iOS Objective-C Project using Split SDK example +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 3 +--- + +

+ +

+ +[iOS Objective-C Project using Split SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/iOS-Objective-C-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-swift-app-two-factories.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-swift-app-two-factories.md new file mode 100644 index 00000000000..ee61370b1e9 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-swift-app-two-factories.md @@ -0,0 +1,13 @@ +--- +title: iOS Swift App Project using Two Split SDK Factories example +sidebar_label: iOS Swift App Project using Two Split SDK Factories example +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 10 +--- + +

+ +

+ +[iOS Swift App Project using Two Split SDK factories example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/iOS-two-factories-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/javascript-code.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/javascript-code.md new file mode 100644 index 00000000000..b9370f75223 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/javascript-code.md @@ -0,0 +1,13 @@ +--- +title: Javascript Code using Split SDK example +sidebar_label: Javascript Code using Split SDK example +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 11 +--- + +

+ +

+ +[Javascript Code using Split SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/Javascript-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/javascript-sdk-nextjs.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/javascript-sdk-nextjs.md new file mode 100644 index 00000000000..4fa62b38e61 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/javascript-sdk-nextjs.md @@ -0,0 +1,13 @@ +--- +title: Javascript SDK Example using Next.js +sidebar_label: Javascript SDK Example using Next.js +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 6 +--- + +

+ +

+ +[Javascript SDK Example using Next.JS](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/JavasScript-with-NextJS) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/javascript.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/javascript.md new file mode 100644 index 00000000000..81b9764a9e8 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/javascript.md @@ -0,0 +1,15 @@ +--- +title: JavaScript SDK used with JavaScript Frameworks +sidebar_label: JavaScript SDK used with JavaScript Frameworks +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 15 +--- + +

+ +

+ +If it’s a JavaScript framework, Split almost certainly supports it. Whether it’s the hot new library out of the world’s largest social network or a project you’ve spun up at home, Split’s JavaScript SDK should work. + +Check out our [Languages](https://www.split.io/product/languages/) section for a high-level look at our JavaScript SDK, or dive into Split’s [JavaScript documentation](https://docs.split.io/docs/javascript-sdk-overview) for more. In particular, at the bottom of that article you will find some [example apps](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#example-apps). \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/nodejs-with-react-redux-using-javascript-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/nodejs-with-react-redux-using-javascript-sdk.md new file mode 100644 index 00000000000..c91c2b6a511 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/nodejs-with-react-redux-using-javascript-sdk.md @@ -0,0 +1,41 @@ +--- +title: Node.js with React Redux Project using Split Javascript SDK example +sidebar_label: Node.js with React Redux Project using Split Javascript SDK example +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 12 +--- + +

+ +

+ +Example: Basic Code to use Javascript Split SDK 10.3.3 + +Environment: + +React 16.4.2 + +Redux 4.0.0 + +React-redux 5.0.7 + +Node Module (npm): 5.6.0 + +How to use: + +Run the commands below to download dependencies: + +* `npm install` + +Update relevant code: + +* Open the `./src/Split.js` and replace authorization key, client keys and traffic types. + +* Open the `./src/constants/features` and list the features you want to evaluate. + +Command to start: `npm start` + +HTTP Access: http://localhost:4200/ + +[Click link to review repo](https://github.com/splitio/react-redux-sdk-examples) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-android-app.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-android-app.md new file mode 100644 index 00000000000..89356bcc349 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-android-app.md @@ -0,0 +1,13 @@ +--- +title: React Native Android App using Split SDK example +sidebar_label: React Native Android App using Split SDK example +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 9 +--- + +

+ +

+ +[React Native Android App using Split SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/React-native-Android-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-app-nodejs.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-app-nodejs.md new file mode 100644 index 00000000000..7c1f75ef441 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-app-nodejs.md @@ -0,0 +1,98 @@ +--- +title: React Native App using Split NodeJS SDK example +sidebar_label: React Native App using Split NodeJS SDK example +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 4 +--- + +

+ +

+ +Example: Basic example for React Native App Project using Split Javascript SDK + +Example Repo: https://github.com/splitio/react-native-sdk-example + +This project was bootstrapped with Expo-CLI using its Managed Workflow. + +``` +$ npm install -g expo-cli +$ expo init react-native-sdk-example +$ cd react-native-sdk-example/ +$ npm install --save @splitsoftware/splitio # or 'yarn add @splitsoftware/splitio' if using yarn dependency manager +``` + +Additionally, Split SDK can be used with React-Native-CLI. You can take a look at the [React Native getting started guide](https://facebook.github.io/react-native/docs/getting-started.html) if you want to test on your own application. + +``` +$ npm install -g react-native-cli +$ react-native init ReactNativeSdkExample +$ cd ReactNativeSdkExample/ +$ npm install --save @splitsoftware/splitio # or 'yarn add @splitsoftware/splitio' if using yarn dependency manager +``` + +When running you should see a screen like the image below (taken from an Android device). + +![](https://help.split.io/hc/article_attachments/360057415851/mobile_screenshot.png) + +## Prerequisites +You'll need [NodeJS](https://nodejs.org/en/download/). We recommend that you use the latest LTS version. + +Second thing you'll need is to install [Expo-CLI](https://expo.io/) with the command `npm install -g expo-cli`. + +To run the app, first change the '' string in the App.js file with the browser key of your Split environment. Optionally, you can try a localhost configuration as the example below: + +```javascript +const factory = SplitFactory({ + core: { + authorizationKey: 'localhost', + key: 'react_native_example' + }, + features: { + 'Test_Split': 'on', + 'Test_Another_Split': 'dark', + 'Test_Something_Else': 'off' + } +}); +``` + +Then call `npm install` and `npm start`. If any error rises when trying to run the app in the Android or iOS emulator, consider updating Expo-Cli or reinstalling the Expo app in your emulator. + +More information on the available scripts below. + +## Available Scripts +If Yarn was installed when the project was initialized, then dependencies will have been installed via Yarn, and you should probably use it to run these commands as well. Unlike dependency installation, command running syntax is identical for Yarn and NPM at the time of this writing. + +### `npm start` +Runs your app in development mode. + +Open it in the [Expo app](https://expo.io/) on your phone to view it. It will reload if you save edits to your files, and you will see build errors and logs in the terminal. + +### `npm test` +Runs the [jest](https://github.com/facebook/jest) test runner on your tests. + +:::note +No test cases have been added since this is an example app. If you're looking for how to test with Split SDK, you can: + +* mock the module import, see Jest documentation for that [here](https://facebook.github.io/jest/docs/en/jest-object.html#jestmockmodulename-factory-options) +* use the localhost (offline) mode of the JavaScript Split SDK, more information [here](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#localhost-mode) +::: + +### `npm run ios` +Like `npm start`, but also attempts to open your app in the iOS Simulator if you're on a Mac and have it installed. + +### `npm run android` +Like `npm start`, but also attempts to open your app on a connected Android device or emulator. Requires an installation of Android build tools (see [React Native docs](https://facebook.github.io/react-native/docs/getting-started.html) for detailed setup). We also recommend installing Genymotion as your Android emulator. Once you've finished setting up the native build environment, there are two options for making the right copy of adb available to Create React Native App: + +### Using Android Studio's `adb` +1. Make sure that you can run adb from your terminal. +2. Open Genymotion and navigate to `Settings -> ADB`. Select “Use custom Android SDK tools” and update with your [Android SDK directory](https://stackoverflow.com/questions/25176594/android-sdk-location). + +### Using Genymotion's `adb` +1. Find Genymotion’s copy of adb. On macOS for example, this is normally `/Applications/Genymotion.app/Contents/MacOS/tools/`. +2. Add the Genymotion tools directory to your path (instructions for [Mac](http://osxdaily.com/2014/08/14/add-new-path-to-path-command-line/), [Linux](http://www.computerhope.com/issues/ch001647.htm), and [Windows](https://www.howtogeek.com/118594/how-to-edit-your-system-path-for-easy-command-line-access/)). +3. Make sure that you can run adb from your terminal. + +### `npm run eject` +This will start the process of "ejecting" from Create React Native App's build scripts. You'll be asked a couple of questions about how you'd like to build your project. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-ios-app.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-ios-app.md new file mode 100644 index 00000000000..3484403b3e4 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-ios-app.md @@ -0,0 +1,13 @@ +--- +title: React Native iOS App using Split SDK example +sidebar_label: React Native iOS App using Split SDK example +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 8 +--- + +

+ +

+ +[React Native iOS App using Split SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/React-native-iOS-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/redux-sdk-running-on-client-side.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/redux-sdk-running-on-client-side.md new file mode 100644 index 00000000000..edec4b414ae --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/redux-sdk-running-on-client-side.md @@ -0,0 +1,13 @@ +--- +title: Redux SDK Running on Client Side Example +sidebar_label: Redux SDK Running on Client Side Example +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 5 +--- + +

+ +

+ +[Redux SDK Running on client side example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/Redux-Client-side-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/using-split-in-a-salesforce-lw-component.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/using-split-in-a-salesforce-lw-component.md new file mode 100644 index 00000000000..9fd9a6058c1 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/using-split-in-a-salesforce-lw-component.md @@ -0,0 +1,13 @@ +--- +title: Using Split in a SalesForce Lightning Web Component +sidebar_label: Using Split in a SalesForce Lightning Web Component +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 2 +--- + +

+ +

+ +[Demo showing how you can use Split in a SalesForce Lightning Web Component](https://github.com/kleinjoshuaa/Split-SFDX-Demo#readme) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/using-split-with-multiple-web-components.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/using-split-with-multiple-web-components.md new file mode 100644 index 00000000000..5eb5bfa824e --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/using-split-with-multiple-web-components.md @@ -0,0 +1,21 @@ +--- +title: Using Split with multiple web components and a single factory instance +sidebar_label: Using Split with multiple web components and a single factory instance +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 1 +--- + +

+ +

+ +## Using Split SDK in a micro frontend environment +This code example, contributed by Joshua Klein, shows how to employ a shared Split module injected into each of multiple micro frontend JS files. This approach allows for independent development and tooling without having multiple Split factory instances running the in the same browser. + +https://github.com/kleinjoshuaa/Multiple-Web-Components + +## What are micro frontends? +Micro Frontends are a relatively new architectural style that involves extending the concept of microservices to the front end of an application. Essentially, in a micro frontend architecture, each UI module is developed, deployed, and maintained independently. This allows independent teams to move faster and have more control over their individual components. Similarly to microservices this requires well-defined interfaces and APIs to ensure that intercommunication between micro frontends scales and is maintainable. + +In this [example](https://github.com/kleinjoshuaa/Multiple-Web-Components) we will show how to use Split as a shared utility that is in scope for each micro frontend. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-ios-javascript-sdk-client-on-never-runs.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-ios-javascript-sdk-client-on-never-runs.md new file mode 100644 index 00000000000..2d2eded28f9 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-ios-javascript-sdk-client-on-never-runs.md @@ -0,0 +1,113 @@ +--- +title: "Mobile SDK: When using client.on method, the code block never called" +sidebar_label: "Mobile SDK: When using client.on method, the code block never called" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 22 +--- + +

+ +

+ +## Issue + +Using Javascript browser-side, Android or iOS SDKs, and implementing the code below, the code block never gets executed which indicates SDK_READY event never fires. + +```javascript +client.on(SplitEvent.SDK_READY, new SplitEventTask() { + treatment = client.getTreatment("Split Name") +}); +``` + +## Root Cause + +The SDK_READY event will fire only once when the SDK factory downloads all the information it needs to calculate the treatment from Split cloud If the code above is executed after the SDK_READY event fires, then the block inside will never be executed since SDK_READY event already fired and will not fire again. + +## Solution + +Even if the cache existed prior to initializing the SDK Factory object, it will always make an http call to Split cloud to sync for any changes before firing SDK_READY event. This means if we execute client.on line immediately after the factory initialization line it will be guaranteed the SDK_READY event fires after client.on is executed. +We recommend to create a wrapper class for the SDK, define isSDKReady property and set it to true inside the client.on block. + +Below are examples per each SDK language: + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + + + + +In the Android wrapper class below, we are using a static variable to indicate the SDK is ready, this will work if you have only one instance of the wrapper class. + +```java +public class SplitSDK { + public SplitClient client; + public static boolean isSDKReady=false; + SplitSDK(String APIKey, Key userId, Context appContext) throws Exception { + SplitClientConfig config = SplitClientConfig.builder() + .build(); + try { + SplitFactory splitFactory = SplitFactoryBuilder.build(APIKey, userId, config, appContext); + this.client = splitFactory.client(); + this.client.on(SplitEvent.SDK_READY, new SplitEventTask() { + @Override + public void onPostExecutionView(SplitClient client) { + SplitSDK.isSDKReady = true; + } + }); + } catch (Exception e) { + System.out.print("Exception: " + e.getMessage()); + } + } +} +``` + + + + +```swift +class SplitWrapper { + var isSDKReady=false + var client:SplitClient + init(apiKey: String, key: Key) { + let config = SplitClientConfig() + let builder = DefaultSplitFactoryBuilder() + let factory = builder.setApiKey(apiKey) + .setKey(key) + .setConfig(config) + .build() + self.client = factory!.client + self.client.on(event: SplitEvent.sdkReady) { + self.isSDKReady=true + } + } +} +``` + + + + +```javascript +class SplitIO { + constructor() { + this.isSDKReady=false; + this.factory = splitio({ + core: { + authorizationKey: APIKEY, + key: userKey, + trafficType: userTrafficType + }, + startup: { + readyTimeout: 4 + }, + }); + this.client = this.factory.client(); + this.client.on(this.client.Event.SDK_READY, () => { + this.isSDKReady=true; + }); + } +} +``` + + + \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-calling-client-destroy-does-not-post-impressions.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-calling-client-destroy-does-not-post-impressions.md new file mode 100644 index 00000000000..b8a6fc1f915 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-calling-client-destroy-does-not-post-impressions.md @@ -0,0 +1,26 @@ +--- +title: Calling client.Destory does not post impressions in Android SDK +sidebar_label: Calling client.Destory does not post impressions in Android SDK +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 10 +--- + +

+ +

+ +## Issue + +When using Android SDK in an app, before the app exits, calling client.Destroy() is suppose to clear the SDK cache and post all impressions; however, the cached impressions are not showing up Live tail tab in Split user interface. + +## Root Cause + +The `client.Destory()` will post any cached impressions, however, if the app shutdown its process before or during the post request, the request will fail and no impressions are posted to Split cloud. + +## Answer + +To resolve the issue, there are two options: + +* This is the recommended action, add to the app workflow calling client.Flush() method which will post the cached impressions and events to Split cloud. +* Add a 2-3 seconds sleep or delay after the `client.Destory()` to allow enough time to post the impressions before the app exits, the amount of seconds will be depend on how fast the network though, it is recommended to adjust it accordingly. diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-does-the-sdk-use-sharedpreferences.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-does-the-sdk-use-sharedpreferences.md new file mode 100644 index 00000000000..445f22b6c03 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-does-the-sdk-use-sharedpreferences.md @@ -0,0 +1,17 @@ +--- +title: "Android SDK: Does the SDK use SharedPreferences on the device to store the Split cache?" +sidebar_label: "Android SDK: Does the SDK use SharedPreferences on the device to store the Split cache?" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 23 +--- + +

+ +

+ +## Question +Does the Android SDK utilize the SharedPreferences on the device to store the Split cache? + +## Answer +The Android SDK does not use the device SharedPreferences. It stores the Split cache directly on internal storage, in the application's context folder. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-duplicate-class-finalizablereferencequeue.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-duplicate-class-finalizablereferencequeue.md new file mode 100644 index 00000000000..ed96d54747f --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-duplicate-class-finalizablereferencequeue.md @@ -0,0 +1,24 @@ +--- +title: "Android SDK: Duplicate class FinalizableReferenceQueue$DirectLoader in modules checkstyle-5.3-all.jar and guava-18.0.jar" +sidebar_label: "Android SDK: Duplicate class FinalizableReferenceQueue$DirectLoader in modules checkstyle-5.3-all.jar and guava-18.0.jar" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 15 +--- + +

+ +

+ +## Issue + +When compiling the App with Android SDK the error below is reported + +Duplicate class com.google.common.base.FinalizableReferenceQueue$DirectLoader found in modules checkstyle-5.3-all.jar (checkstyle-5.3-all.jar) and guava-18.0.jar (com.google.guava:guava:18.0) +## Root Cause + +Split Android SDK has Google guava 18.0 library as a dependency, while Checkstyle 5.3 has dependency on [com.google.collections](https://mvnrepository.com/artifact/com.google.collections) » [google-collections](https://mvnrepository.com/artifact/com.google.collections/google-collections) 1.0 which is an old library and is causing the duplicate error. + +## Solution + +Upgrade the Checkstyle version to 7.0. It will compile successfully, since that version uses Google guava library instead. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-http-exception-chain-validation-failed.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-http-exception-chain-validation-failed.md new file mode 100644 index 00000000000..9800572ae87 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-http-exception-chain-validation-failed.md @@ -0,0 +1,26 @@ +--- +title: "HTTP Exception: Chain validation failed" +sidebar_label: "HTTP Exception: Chain validation failed" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 18 +--- + +

+ +

+ +## Issue + +When running Android app in Emulator, Split Android SDK shows the error below right after initialization: +``` +io.split.android.client.network.HttpException: HttpException: Error serializing request body: Chain validation failed +``` + +## Root Cause + +This error is coming from the SSL Handshake library, since the SDK is trying to call GET http request to https://sdk.split.io. A possible root cause is the device time is off the current time. + +## Solution + +In the device Config page, make sure the device Date/Time is synched to the current time and restart the app. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-sdk-takes-too-long-to-get-ready.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-sdk-takes-too-long-to-get-ready.md new file mode 100644 index 00000000000..bc17d0f583e --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-sdk-takes-too-long-to-get-ready.md @@ -0,0 +1,29 @@ +--- +title: "Android SDK: SDK takes too long to get ready" +sidebar_label: "Android SDK: SDK takes too long to get ready" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 16 +--- + +

+ +

+ +## Issue + +When using Android SDK, the first time the App loads the SDK takes sometime to download definitions from Split cloud and cache them locally. However, when SDK starts afterwards, it still takes long time even though the cache is already downloaded to app file system. + +## Root Cause + +If the version of Android SDK used is 2.4.2 or below, the issue can manifest since the factory object is still making a full data request from Split cloud even if the previous cache exists in app file system. + +## Solution + +Upgrade Android SDK to latest build to fix this issue, + +To prevent your app from waiting indefinitely on Split SDK in case there is an issue with the network, you can listen to SDK_READY_TIMED_OUT with a specific timeout you can set. This will allow your code to move on and not continue to wait on Split SDK. + +Another useful event is SDK_READY_FROM_CACHE, since the first time the SDK runs successfully in the app it will store the cache in the app storage, so the next time the SDK initializes it can use the existing cache and does not need to wait for the network sync. + +For more info please check the [SDK doc](https://help.split.io/hc/en-us/articles/360020343291-Android-SDK#basic-usage). \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-using-kotlin-sdk-always-returns-control-treatment.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-using-kotlin-sdk-always-returns-control-treatment.md new file mode 100644 index 00000000000..9a93a79dca8 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-using-kotlin-sdk-always-returns-control-treatment.md @@ -0,0 +1,38 @@ +--- +title: "Android SDK: Using Kotlin, SDK always returns control treatment" +sidebar_label: "Android SDK: Using Kotlin, SDK always returns control treatment" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 17 +--- + +

+ +

+ +## Issue + +When using Android App with Kotlin language, the code below always returns contro" treatment from Split Android SDK + +```java +val apiKey = "API KEY" +val config = SplitClientConfig.builder().enableDebug().build() +val matchingKey = "userxx" +val bucketKey = null +val key = Key(matchingKey, bucketKey) +val splitFactory = SplitFactoryBuilder.build(apiKey, key, config, applicationContext) +val splitClient = splitFactory.client() + +splitClient.on(SplitEvent.SDK_READY, object : SplitEventTask() { + var treatment = splitClient.getTreatment("split-name") +}) +``` + +## Root Cause + +While this code works fine using Swift language based Projects, in Kotlin the code does not listen to the `SDK_READY` event if used as is. + +## Solution +Based on the [Advanced Section](https://help.split.io/hc/en-us/articles/360020343291-Android-SDK#advanced-subscribe-to-events) of Android SDK documentation, we can override onPostExecution function, which will be only called when the `SDK_READY` event fires. + +
splitClient.on(SplitEvent.SDK_READY, object : SplitEventTask() \{

override fun onPostExecution(client: SplitClient) \{

var treatment = splitClient.getTreatment("split-name")

\}

\})
\ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/browser-sdk-migration-guide.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/browser-sdk-migration-guide.md new file mode 100644 index 00000000000..2cb82fa78a5 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/browser-sdk-migration-guide.md @@ -0,0 +1,136 @@ +--- +title: Browser SDK migration guide +sidebar_label: Browser SDK migration guide +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 6 +--- + +

+ +

+ +Refer to this document to check API differences and migration details for moving from **Javascript SDK v10.15.x** using [NPM](https://www.npmjs.com/package/@splitsoftware/splitio) or [Github](https://github.com/splitio/javascript-client) to **Browser SDK v0.1.x** using [NPM](https://www.npmjs.com/package/@splitsoftware/splitio-browserjs) or [Github](https://github.com/splitio/javascript-browser-client). + +## Requirements + +Browser SDK has the [same browser requirements](https://help.split.io/hc/en-us/articles/360058730852-Browser-SDK#language-support) as Javascript SDK (it supports ES5 syntax and requires Promises support) but also requires [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) support. + +Therefore, to target old browsers such as IE10, users must polyfill the Fetch API besides Promises. More details [here](https://help.split.io/hc/en-us/articles/360058730852-Browser-SDK#language-support). + +## Installation and import + +| | Javascript SDK 10.15.x | Browser SDK 0.1.x | +| --- | --- | --- | +| Install NPM package | `> npm install @splitsoftware/splitio` | `> npm install @splitsoftware/splitio-browserjs` | +| Import with ES6 module syntax | `import { SplitFactory } from ‘@splitsoftware/splitio’` | `import { SplitFactory } from ‘@splitsoftware/splitio-browserjs’` | +| Import with CommonJS | `const { SplitFactory } = require( ‘@splitsoftware/splitio’)` | `const { SplitFactory } = require( ‘@splitsoftware/splitio-browserjs’)` | +| Install with CDN script tag | `` | Two variants are available at the moment:
Slim/Regular version:
``
Full version: regular + pluggable modules (InLocalStorage, integrations and loggers)
`` | +| Instantiate with CDN | `var factory = splitio({...})` | `var factory = splitio({...})` or `var factory = splitio.SplitFactory({...})` | + +## Configuration and API + +Most configuration params are the same in [Javascript SDK](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#configuration) and [Browser SDK](https://help.split.io/hc/en-us/articles/360058730852-Browser-SDK#configuration). SDK client and manager APIs (i.e., method signatures) are also the same. The differences: + +### Traffic type + +| Javascript SDK 10.15.x | Browser SDK 0.1.x | +| --- | --- | +| Clients can be bound to a traffic type to track events without the need to pass the traffic type.
var factory = SplitFactory(\{

core: \{

authorizationKey: 'YOUR_API_KEY',

key: 'USER_ID',

trafficType: 'YOUR_CUSTOMER_TRAFFIC_TYPE'

\}

\});



// Must not pass traffic type to track call if provided on the factory settings



var mainClient = factory.client();

mainClient.track('EVENT_TYPE', eventValue);



// or when creating a new client with a traffic type.



var newClient = factory.client('NEW_KEY', 'NEW_TRAFFIC_TYPE');

newClient.track('EVENT_TYPE', eventValue);
| Clients cannot be bound to a traffic type, so for tracking events we always need to pass the traffic type. This simplifies the `track` method signature, by removing ambiguity of when it should receive the traffic type or not. |
var factory = SplitFactory(\{

core: \{

authorizationKey: 'YOUR_API_KEY',

key: 'USER_ID',

trafficType: 'YOUR_CUSTOMER_TRAFFIC_TYPE'

\}

\});



// Must not pass traffic type to track call if provided on the factory settings



var mainClient = factory.client();

mainClient.track('EVENT_TYPE', eventValue);



// or when creating a new client with a traffic type.



var newClient = factory.client('NEW_KEY', 'NEW_TRAFFIC_TYPE');

newClient.track('EVENT_TYPE', eventValue);
| NOT ALLOWED
The `core.trafficType` config param, and the second param of `factory.client()` are ignored. | + +### Bucketing key + +Bucketing key support was removed from Browser SDK. So passing an object as a key is not allowed: + +``` javascript +var factory = SplitFactory({ + core: { + authorizationKey: 'YOUR_API_KEY', + + // NOT SUPPORTED: key must be a string + key: { matchingKey: 'YOUR_MATCHING_KEY', bucketingKey: 'YOUR_BUCKETING_KEY' } + } +}); + +// NOT SUPPORTED: key must be a string +var newClient = factory.client({ matchingKey: 'NEW_MATCHING_KEY', bucketingKey: 'NEW_BUCKETING_KEY' }); +``` + +### Pluggable modules + +Some configuration params are based on pluggable modules now, in favor of size reduction. + +When using ES module imports, the pluggable modules that are not imported are not included in the final output build. + +The impact of each module on the final bundle size is roughly estimated in the Export Analysis section of [Bundlephobia entry](https://bundlephobia.com/result?p=@splitsoftware/splitio-browserjs@0.1.0). + +Importing pluggable modules with ES module imports: + +```javascript +import { + SplitFactory, + // Pluggable modules: + ErrorLogger, WarnLogger, InfoLogger, DebugLogger, + InLocalStorage, GoogleAnalyticsToSplit, SplitToGoogleAnalytics } +from '@splitsoftware/splitio-browserjs'; +``` +When using CDN script tags, you can opt for using the regular/slim version without pluggable modules or the full one with them. + +Accessing pluggable modules with the full CDN bundle (they are not available on the slim version): + +```javascript +const { + SplitFactory, + // Pluggable modules: + ErrorLogger, WarnLogger, InfoLogger, DebugLogger, + InLocalStorage, GoogleAnalyticsToSplit, SplitToGoogleAnalytics } += window.splitio; +``` + +### Logging + +In the Browser SDK, you must “plug” a logger instance in the `debug` config param to have human-readable message codes. + +You can set a boolean or string log level value as `debug` config param, as in the regular Javascript SDK, but in that case, most log messages will display a code number instead. + +Those message codes are listed in the public repository: [`Error`](https://github.com/splitio/javascript-commons/blob/development/src/logger/messages/error.ts), [`Warning`](https://github.com/splitio/javascript-commons/blob/development/src/logger/messages/warn.ts), [`Info`](https://github.com/splitio/javascript-commons/blob/development/src/logger/messages/info.ts), [`Debug`](https://github.com/splitio/javascript-commons/blob/development/src/logger/messages/debug.ts). More details [here](https://help.split.io/hc/en-us/articles/360058730852-Browser-SDK#logging). + +| Javascript SDK 10.15.x | Browser SDK 0.1.x | +| --- | --- | +|
import \{ SplitFactory \} from '@splitsoftware/splitio'



var factory = SplitFactory(\{

…,

debug: 'DEBUG' // other options are: true, false,

// 'INFO', 'WARN' and 'ERROR'

\});

|
import \{ SplitFactory, DebugLogger \} from '@splitsoftware/splitio-browserjs'



var factory = SplitFactory(\{

…,

debug: DebugLogger() // other options are: true, false, 'DEBUG',

// 'INFO', 'WARN', 'ERROR', InfoLogger(),

// WarnLogger(), and ErrorLogger()

\});
| + +### Configuring LocalStorage + +| Javascript SDK 10.15.x | Browser SDK 0.1.x | +| --- | --- | +|
import \{ SplitFactory \} from '@splitsoftware/splitio'



var sdk = SplitFactory(\{

core: \{

authorizationKey: 'YOUR_API_KEY',

key: 'USER_ID',

\},



storage: \{

type: 'LOCALSTORAGE',

prefix: 'MYPREFIX'

\}

});
|
import \{

SplitFactory,

InLocalStorage

\} from '@splitsoftware/splitio-browserjs'



var sdk = SplitFactory(\{

core: \{

authorizationKey: 'YOUR_API_KEY',

key: 'USER_ID',

},



storage: InLocalStorage(\{

prefix: 'MYPREFIX'

\})

\});
| + + + +## Additional Notes + +CDN bundle size: + +* Javascript SDK (https://cdn.split.io/sdk/split-10.15.4.min.js ): 126656 B (123.7 kB) +* Browser SDK + * Slim/regular (https://cdn.split.io/sdk/split-browser-0.1.0.min.js ): 69338 B (67.7 kB) + * Full (https://cdn.split.io/sdk/split-browser-0.1.0.full.min.js ): 93163 B (91.0 kB) + +NPM package size: + +* [Javascript SDK](https://bundlephobia.com/result?p=@splitsoftware/splitio@10.15.4): 109.7 kB minified +* [Browser SDK](https://bundlephobia.com/result?p=@splitsoftware/splitio-browserjs@0.1.0): 87.2 kB minified \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-and-android-sdk-how-to-initialize-for-multiple-user-ids.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-and-android-sdk-how-to-initialize-for-multiple-user-ids.md new file mode 100644 index 00000000000..060f0c01e97 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-and-android-sdk-how-to-initialize-for-multiple-user-ids.md @@ -0,0 +1,30 @@ +--- +title: "Mobile SDK: How to initialize for multiple user IDs?" +sidebar_label: "Mobile SDK: How to initialize for multiple user IDs?" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 19 +--- + +

+ +

+ +## Question + +The Javascript SDK is capable of initializing multiple client objects from the same Split factory object, each with their unique user key (user id): + +```javascript +client1 = factory.client("user_id1"); +client2 = factory.client("user_id2"); +``` + +However, iOS and Android SDKs do not have this feature, how could it be implemented using those SDKs? + +## Answer + +Since iOS and Android SDKs do not support initializing multiple client objects from the same factory object, the solution is to initialize a second factory object. + +It is important to note the SDK factory object will create the local SDK cache folder and use the SDK API Key for naming convention. Its strongly recommended to use different SDK API Key for each factory object, to have each factory sync and update its own cache folder. + +Checkout the [example code for iOS using two factories](https://help.split.io/hc/en-us/articles/360030632172). \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-android-browser-sdk-does-the-sdk-cache-expire.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-android-browser-sdk-does-the-sdk-cache-expire.md new file mode 100644 index 00000000000..dcd78db1580 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-android-browser-sdk-does-the-sdk-cache-expire.md @@ -0,0 +1,19 @@ +--- +title: "Mobile and web SDK: Does the SDK cache expire?" +sidebar_label: "Mobile and web SDK: Does the SDK cache expire?" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 8 +--- + +

+ +

+ +## Question + +The Split mobile (iOS and Android) and Javascript browser SDKs download a local cache and store it in a file system. Does the cache have an expire date or TTL? + +## Answer + +The SDK will consider the cache stale if it hasn't been updated for 90 days. In such case it will issue a full download of Split definitions. This is an unlikely scenario since the SDK is continuously synching changes from the Split cloud and updating the cache. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-android-browser-sdk-split-changes-roll-out-slowly-to-user-devices.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-android-browser-sdk-split-changes-roll-out-slowly-to-user-devices.md new file mode 100644 index 00000000000..ff8a88679d1 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-android-browser-sdk-split-changes-roll-out-slowly-to-user-devices.md @@ -0,0 +1,30 @@ +--- +title: "Mobile and web SDK: Split changes roll out slowly to user devices." +sidebar_label: "Mobile and web SDK: Split changes roll out slowly to user devices." +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 7 +--- + +

+ +

+ +## Issue + +When making a change to a feature flag through the web UI, mobile (iOS and Android) and Javascript Browser SDKs do not reflect that change at the same time. A small population of devices are synched in the first day, then more user devices get synched in subsequent days until all SDKs are updated. + +Why do Split changes propagate slowly to user devices? + +## Root Cause + +This scenario has two potential root causes: + +* The code in mobile app or web page does not listen to `SDK_READY` event, and thus will use the stored cache from the last user session, which is not synched to latest changes. When the user hits the page the next day, the existing cache will be synched from the previous day, so the change is detected now and reflected in the impression. +* The code in mobile app or web page listens to `SDK_READY_FROM_CACHE` event only and triggers getTreatment calls after that event fires. This event will fire if the SDK detected the cache exists in App/Browser filesystem, which is not synched yet with latest changes. This is why the treatments will always reflect the cache from the previous user session. + +## Solution + +Always calculate treatments after `SDK_READY` event fires. This event fires once the synchronization with Split cloud is complete and will guarantee the latest changes are reflected in the cache. + +While `SDK_READY_FROM_CACHE` is very useful to allow calculating treatments quickly, it is recommended to check the treatment again after `SDK_READY` and reflect the treatment in case there is a change. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-sdk-missing-track-method.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-sdk-missing-track-method.md new file mode 100644 index 00000000000..fa997310983 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-sdk-missing-track-method.md @@ -0,0 +1,23 @@ +--- +title: Is the iOS SDK Split library missing the track method? +sidebar_label: Is the iOS SDK Split library missing the track method? +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 27 +--- + +

+ +

+ +## Issue + +Using Split iOS SDK in Xcode project, when trying to use track method, build error "Value of type 'SplitClientProtocol' has no member 'track'. + +![](https://help.split.io/hc/article_attachments/360010664231/Screen_Shot_2018-09-04_at_9.36.57_AM.png) + +## Root Cause +The Split iOS SDK used is likely an old version that is earlier than 1.3.0. + +## Solution +Make sure to use the latest version in Cocoapod. Refer to the [SDK doc link](https://docs.split.io/docs/ios-sdk-overview). \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-sdk-runtime-error-jfbcrypt-m-left-shift-of-x-by-y-places.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-sdk-runtime-error-jfbcrypt-m-left-shift-of-x-by-y-places.md new file mode 100644 index 00000000000..4587dc1c18c --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-sdk-runtime-error-jfbcrypt-m-left-shift-of-x-by-y-places.md @@ -0,0 +1,34 @@ +--- +title: "iOS SDK runtime error: JFBCrypt.m left shift of [x] by [y] places cannot be represented in type 'SInt32'" +sidebar_label: "iOS SDK runtime error: JFBCrypt.m left shift of [x] by [y] places cannot be represented in type 'SInt32'" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 13 +--- + +

+ +

+ +## Issue + +Using Objective-C Project with iOS SDK, the following runtime error shows as soon as the Split factory object is initialized: + +``` +.../Pods/Split/Split/Common/Utils/JFBCrypt/JFBCrypt.m:578:16: runtime error: left shift of 16488694 by 8 places cannot be represented in type 'SInt32' (aka 'int') +``` + +## Root Cause + +If Undefined Behavior Sanitizer flag is turned on, it will cause the error above. + +![](https://help.split.io/hc/article_attachments/360060917591/Screen_Shot_2020-06-30_at_08.46.33.png) + +## Answer + +To resolve the issue, follow steps below: + +1. Turn off the Undefined Behavior Sanitizer flag located at the Diagnostics tab in your target's Edit Scheme option. +2. Clear the project. +3. Delete the derived data folder. +4. Rebuild. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-cors-error-in-streaming-call-when-running-sdk-in-service-worker.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-cors-error-in-streaming-call-when-running-sdk-in-service-worker.md new file mode 100644 index 00000000000..daaa3a4a98a --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-cors-error-in-streaming-call-when-running-sdk-in-service-worker.md @@ -0,0 +1,102 @@ +--- +title: "Javascript SDK: CORS Error in streaming call when running SDK in Service Worker" +sidebar_label: "Javascript SDK: CORS Error in streaming call when running SDK in Service Worker" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 4 +--- + +

+ +

+ +## Issue + +When running the Javascript SDK inside Service Worker, the SDK Streaming http call to streaming.split.io is blocked by CORS browser policy as shown below: + +![](https://help.split.io/hc/article_attachments/4415274038285) + +## Root cause + +The Service Worker acts as a proxy between the browser and the network. By intercepting requests made by the document, service workers can redirect requests to a cache, enabling offline access. + +A request interception occurs and the application that makes use of this technology requires definitions about how to handle certain requests (fetch) and return a result to the browser/DOM. + +In the case of the JS SDK, the Service Worker can receive Push Notifications, which are the specific type of connection called [Server-Side Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) (SSE). These notifications must be defined into the Service Worker to allow the Stream connection between our SDK and Split's backend to work. + +If you use a Service Worker as a proxy, and you hook the fetch request in a particular way (e.g., adding the `cache-control` header) but don't take the SSE types into consideration, it might lead to a CORS issue. + +## Answer + +The following is an example of what you can do to manipulate the SSE streams connections (only): +``` +self.addEventListener('fetch', event => { + const {headers, url} = event.request; + const isSSERequest = headers.get('Accept') === 'text/event-stream'; + + // We process only SSE connections + if (!isSSERequest) { + return; + } + + // Response Headers for SSE + const sseHeaders = { + 'content-type': 'text/event-stream', + 'Transfer-Encoding': 'chunked', + 'Connection': 'keep-alive', + }; + // Function formatting data for SSE + const sseChunkData = (data, event, retry, id) => + Object.entries({event, id, data, retry}) + .filter(([, value]) => ![undefined, null].includes(value)) + .map(([key, value]) => `${key}: ${value}`) + .join('\n') + '\n\n'; + // Table with server connections, where key is url, value is EventSource + const serverConnections = {}; + // For each url, we open only one connection to the server and use it for subsequent requests + const getServerConnection = url => { + if (!serverConnections[url]) serverConnections[url] = new EventSource(url); + + return serverConnections[url]; + }; + // When we receive a message from the server, we forward it to the browser + const onServerMessage = (controller, {data, type, retry, lastEventId}) => { + const responseText = sseChunkData(data, type, retry, lastEventId); + const responseData = Uint8Array.from(responseText, x => x.charCodeAt(0)); + controller.enqueue(responseData); + }; + const stream = new ReadableStream({ + start: controller => getServerConnection(url).onmessage = onServerMessage.bind(null, controller) + }); + const response = new Response(stream, {headers: sseHeaders}); + + event.respondWith(response); +}); + ``` + +:::info +If a defaultHandler is set to `NetworkFirst [setDefaultHandler(newNetworkFirst());]`, that could prevent the event listener from firing. Removing the default handler fixes this. +::: + +An alternate example returns false from the listener as shown below: +``` +self.addEventListener('fetch', event => { +// no caching for chrome-extensions + if (event.request.url.startsWith('chrome-extension:')) { + return false; + } +// prevent header striping errors from workbox strategies for EventSource types + if (event.request.url.includes('streaming.split.io')) { + return false; + } +// prevent non-cacheable post requests + if (event.request.method != 'GET') { + return false; + } +// all others, use NetworkFirst workbox strategies + if (strategies) { + const networkFirst = new strategies.NetworkFirst(); + event.respondWith(networkFirst.handle({ request: event.request })); + } +}); +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-does-sdk-ready-event-fire-only-once.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-does-sdk-ready-event-fire-only-once.md new file mode 100644 index 00000000000..7bca6a3db41 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-does-sdk-ready-event-fire-only-once.md @@ -0,0 +1,37 @@ +--- +title: "Javascript SDK: Does SDK_READY event fire only once?" +sidebar_label: "Javascript SDK: Does SDK_READY event fire only once?" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 9 +--- + +

+ +

+ +## Problem + +When implementing the code below, sometimes the code never gets executed even though no errors are captured from the SDK Error log. + +```javascript +client.on(client.Event.SDK_READY, function() { + var treatment = client.getTreatment("SPLIT_NAME"); + console.log("Treatment = "+treatment); +}); +``` + +## Root Cause + +The `SDK_READY` fires only once, so if the code block above is executed **after** the `SDK_READY` event is fired, it will never be triggered. + +## Solution + +Another option to check for SDK ready is using the built-in Promise `client.ready()`, this can be used anytime, which gives it more advantage over checking the event only, see the example below: + +```javascript +client.ready().then(() => { + var treatment = client.getTreatment("SPLIT_NAME"); + console.log("Treatment = "+treatment); +}); +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-error-shared-client-not-supported-by-the-storage-mechanism.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-error-shared-client-not-supported-by-the-storage-mechanism.md new file mode 100644 index 00000000000..6d1d8742364 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-error-shared-client-not-supported-by-the-storage-mechanism.md @@ -0,0 +1,39 @@ +--- +title: "Javascript SDK Error: \"Shared Client not supported by the storage mechanism. Create isolated instances instead\"" +sidebar_label: "Javascript SDK Error: \"Shared Client not supported by the storage mechanism. Create isolated instances instead\"" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 20 +--- + +

+ +

+ +## Issue + +When testing Javascript SDK browser mode using Jest, it fails with the following error: + +Shared Client not supported by the storage mechanism. Create isolated instances in stead + +## Root cause + +When using Jest for testing applications, Jest runs in NodeJS by default, and NodeJS does not support shared clients, which is why it detects the storage does not have that function. +It is not possible to overwrite that method from the outside. + +## Solution + +You can instruct Jest to explicitly resolve to browser by setting the config in jest options. For example, when using package.json we can add the flag: +``` +{ + "name": "MYAPP", + "version": "X.X.X", + .... + "jest": { + "browser": true + } + ... +} +``` + +See the [Jest documentation](https://jestjs.io/docs/en/configuration#browser-boolean) for more information. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-how-to-deploy-javascript-sdk-to-a-wordpress-site.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-how-to-deploy-javascript-sdk-to-a-wordpress-site.md new file mode 100644 index 00000000000..75d2ae92235 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-how-to-deploy-javascript-sdk-to-a-wordpress-site.md @@ -0,0 +1,66 @@ +--- +title: How to deploy Javascript SDK to a Wordpress site? +sidebar_label: How to deploy Javascript SDK to a Wordpress site? +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 14 +--- + +

+ +

+ +## Question + +How to deploy Javascript SDK code in a Wordpress site? + +## Answer + +The steps below explain how to use Javascript SDK in a blank page within a Wordpress site + +1. First step is to install Header and Footer Scripts plugin. While this is not required, it is a good practice to load the Javascript SDK library within the page header. + +![](https://help.split.io/hc/article_attachments/360060037831/Screen_Shot_2020-06-18_at_11.25.35_AM.png) + +2. In your Site Development page, create a sample page. + +![](https://help.split.io/hc/article_attachments/360060038051/Screen_Shot_2020-06-18_at_11.34.28_AM.png) + +3. Open the page in Edit mode, scroll to the bottom and paste the import script tag under **Insert Script to \** section. + +``` + +``` + +![](https://help.split.io/hc/article_attachments/360060038491/Screen_Shot_2020-06-18_at_1.37.15_PM.png) + +4. Click on the **+** sign to insert new block, and choose **Custom HTML**. + +![](https://help.split.io/hc/article_attachments/360059871812/Screen_Shot_2020-06-18_at_11.54.52_AM.png) + +5. The Custom HTML block allows any HTML elements, including Javascript, copy and paste the code below inside it, make sure to replace the API KEY with a valid key, set the User key and feature flag name as well. + +```javascript +

+ +``` + +6. Save and review the page, once it loads, the treatment is calculated after the SDK_READY event fires and display the feature flag name and treatment value, in the \ section. + +![](https://help.split.io/hc/article_attachments/360060039131/Screen_Shot_2020-06-18_at_1.43.36_PM.png) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-how-to-enable-conent-security-policy.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-how-to-enable-conent-security-policy.md new file mode 100644 index 00000000000..bbb00062f95 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-how-to-enable-conent-security-policy.md @@ -0,0 +1,34 @@ +--- +title: "Javascript SDK: How to enable Content Security Policy (CSP) to work with Javascript SDK" +sidebar_label: "Javascript SDK: How to enable Content Security Policy (CSP) to work with Javascript SDK" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 21 +--- + +

+ +

+ +## Question + +Is it possible to enable SCP (Content Security Policy) on a site that uses Split Javascript SDK? + +## Answer + +Content Security Policy (CSP) is a computer security standard introduced to prevent cross-site scripting (XSS), clickjacking and other code injection attacks, as defined by this wikipedia article. + +It is possible to allow SCP and enable running Split Javascript SDK safely. + +There are multiple ways to achieve this, the steps below use "nonce" keyword to target the script block. + +Make sure the server response header contains the following: +Content-Security-Policy: script-src 'self' cdn.split.io 'nonce-swfT4W3546RtDw4'; +On the Javascript side, use this tag for the JS code that uses the SDK: +``` + +``` + +The nonce key is any random characters generated, just make sure the response and script tags keys are matched. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-localhost-mode-does-not-support-allowlist-keys.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-localhost-mode-does-not-support-allowlist-keys.md new file mode 100644 index 00000000000..fe8b55f799e --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-localhost-mode-does-not-support-allowlist-keys.md @@ -0,0 +1,51 @@ +--- +title: "Javascript SDK: localhost mode does not support Allowlist keys" +sidebar_label: "Javascript SDK: localhost mode does not support Allowlist keys" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 2 +--- + +

+ +

+ +## Question + +Javascript, React, Redux, and Browser SDKs use features config parameter to set the feature flags and treatments names, however, it does not support adding Allowlist keys in the property. + +How can we mimic allowing keys to get certain treatments similar to the yaml file structure used for Server side SDKs? + +## Answer + +Until the Allowlist option is added to the `features` config parameter, we can use multiple feature flags array to flip between the treatments based on the key, and thus we can mimic allowing certain key to get certain treatment. + +See example below: +```javascript +const myFeatures = { + agus: { + flag1: 'on', + flag2: 'off' + }, + sanjay: { + flag1: 'off', + flag2: 'on' + }, + default: { + flag1 : 'off', + flag2: 'off' + } + }; + +const config = { + core: { + authorizationKey: 'localhost', + key: myKey + }, + features: myFeatures[myKey] || myFeatures['default'], + startup: { + readyTimeout: 5, // 5 sec + retriesOnFailureBeforeReady: 3 //3 retries + } +} +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-mysegments-endpoint.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-mysegments-endpoint.md new file mode 100644 index 00000000000..f3a23b232d8 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-mysegments-endpoint.md @@ -0,0 +1,38 @@ +--- +title: "Why does the Javascript URL \"https://sdk.split.io/api/mySegments/\" return HTTP 404 error?" +sidebar_label: "Why does the Javascript URL \"https://sdk.split.io/api/mySegments/\" return HTTP 404 error?" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 28 +--- + +

+ +

+ +## Problem +Using Javascript SDK, its generating URL below with 404 errors +``` +GET https://sdk.split.io/api/mySegments/ 404 +``` + +## Root Cause + +The URL https://sdk.split.io/api/mySegments/ is missing the key id which is why we are seeing 404 errors. For example, if the key ID (or customer ID) is set to 8879, then this URL will look like: +``` +GET https://sdk.split.io/api/mySegments/8879 +``` + +## Solution + +Make sure to specify the key or customer ID correctly in the factory initializer line or when fetching client object line: + +```javascript +var factory = splitio({ + core: { + authorizationKey: 'YOUR_API_KEY', + key: 'CUSTOMER_ID', + trafficType: 'TRAFFIC_TYPE' + } +var user_client = factory.client('CUSTOMER_ID', 'TRAFFIC_TYPE'); +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-not-ready-status-in-slow-networks.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-not-ready-status-in-slow-networks.md new file mode 100644 index 00000000000..ea14c2cbe7f --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-not-ready-status-in-slow-networks.md @@ -0,0 +1,54 @@ +--- +title: Why does the Javascript SDK return Not Ready status in Slow Networks? +sidebar_label: Why does the Javascript SDK return Not Ready status in Slow Networks? +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 26 +--- + +

+ +

+ +## Issue + +When using JavaScript SDK in Browser, the SDK status will mostly return Not Ready when users are on a slow Network (for example 3G). + +## Root Cause + +It takes a long time for the SDK to fetch the feature flags and Segments Information data from Split cloud due to slow network, which might cause control treatments. + +## Solution + +You need to Increase the startup.readyTimeout value to ensure it covers the SDK fetching Split configuration time. + +As explained in https://docs.split.io/docs/javascript-sdk-overview under Configuration section, the default value for startup.requestTimeoutBeforeReady is 1.5 seconds. + +Follow the steps below to implement the solution: + +1. Find out how long it takes the browser to fetch the Split configuration under slow Network, Chrome Dev tools can be used to simulate 3G Network. +2. Make sure to enable the Java SDK console debug logging by running the following command in the browser Javascript console: + ``` +localStorage.splitio_debug = 'on' +``` +3. Load your page and check the debug logging, look for the following line: + ``` +[TIME TRACKER]: [Fetching - Splits] took xxxx ms to finish +``` + Where xxxx is the total time in milliseconds. + +4. Set the `requestTimeoutBeforeReady` and `readyTimeout` parameters to a higher value than the total fetching time. Example: + ``` +var sdk = SplitFactory({ +startup: { + requestTimeoutBeforeReady: 5, + readyTimeout: 5 +}, +``` +5. In addition to the above, enable using the browser cache to store the Split configuration, to avoid using the network each time the Split configuration data is needed. You only need to specify the the structure below when initializing your SDK object: + ``` +storage: { + type: 'LOCALSTORAGE', + prefix: 'MYPREFIX' +}, +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-polimer-cli-enoent-error.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-polimer-cli-enoent-error.md new file mode 100644 index 00000000000..ca2bc4e887e --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-polimer-cli-enoent-error.md @@ -0,0 +1,47 @@ +--- +title: "Building Javascript SDK using polymer-cli cause error: ENOENT: no such file or directory" +sidebar_label: "Building Javascript SDK using polymer-cli cause error: ENOENT: no such file or directory" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 25 +--- + +

+ +

+ + +## Issue + +### Build environment + +@polymer/polymer: 3.1.0 +polymer-cli: 1.9.6 + +### Steps to reproduce + +``` +npm i @splitsoftware/splitio@10.6.0 +Import via es module: import { SplitFactory } from '@splitsoftware/splitio'; +run polymer build +The following error appears: +Error: ENOENT: no such file or directory, open '/Users/[USER_NAME]/projects/[PROJECT_NAME]/frontend/events' +``` + +## Answer + +The way Polymer is performing the build differs significantly from webpack and other bundlers that can recognize the right path for an isomorphic app. +Polymer is trying to load the Node code path of the SDK, which in turn tries to import the events module from NodeJS. + +Taking a look to what's on node_modules\@splitsoftware\splitio folder, you'll see a few package.json files with this format: + +```json +{ + "main": "./node.js", + "browser": "./browser.js" +} +``` + +What we do there is tell NodeJS to just run the Node version of that module (the main field), while we tell bundlers to use the "Browser version". + +If the plan is to implement the JS SDK in both server and browser modes, make sure to set the browser and main values to the corresponding js code. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-react-native.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-react-native.md new file mode 100644 index 00000000000..a3d19eccbdb --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-react-native.md @@ -0,0 +1,35 @@ +--- +title: "Running bundle using React Native and Javascript SDK causes an error. Bundling failed: Error: Unable to resolve module `util`" +sidebar_label: "Running bundle using React Native and Javascript SDK causes an error. Bundling failed: Error: Unable to resolve module `util`" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 24 +--- + +

+ +

+ +## Issue + +### Build environment + +react: 16.3.0 +react-native: 0.55.0 + +### Steps to reproduce + +When running the bundle, following error occurrs: bundling failed: Error: Unable to resolve module `util` from `D:\\project\node_modules\@splitsoftware\splitio\lib\utils\logger\LoggerFactory.js: Module util does not exist in the Haste module map` + +## Answer + +Javascript SDK requires class util as a dependency; however, it's not included in the package.json dependencies, since it comes built-in in most npm packages. + +React Native, however, does not have the class "util" by default installation. + +Install the class using the command below, this will fix the issue +``` +npm install util +``` + +As of July 29th 2021, our [React Native SDK](https://help.split.io/hc/en-us/articles/4406066357901-React-Native-SDK) is available. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-error-building-app-with-webpack.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-error-building-app-with-webpack.md new file mode 100644 index 00000000000..dcefc99917d --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-error-building-app-with-webpack.md @@ -0,0 +1,45 @@ +--- +title: "React SDK: Error building app with webpack \"Entrypoint undefined = ng/index.html\"" +sidebar_label: "React SDK: Error building app with webpack \"Entrypoint undefined = ng/index.html\"" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 11 +--- + +

+ +

+ +## Issue + +React App failed to build using Webpack after installing React SDK: + +``` +1 asset +Entrypoint undefined = ng/admin_index.html +[./node_modules/html-webpack-plugin/lib/loader.js!./admin_index.html] 1.89 KiB {0} [built] +[./node_modules/html-webpack-plugin/node_modules/lodash/lodash.js] 527 KiB {0} [built] +[./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 472 bytes {0} [built] +[./node_modules/webpack/buildin/module.js] (webpack)/buildin/module.js 497 bytes {0} [built] +Child html-webpack-plugin for "ng\index.html": +1 asset +Entrypoint undefined = ng/index.html +[./node_modules/html-webpack-plugin/lib/loader.js!./index.html] 1.91 KiB {0} [built] +[./node_modules/html-webpack-plugin/node_modules/lodash/lodash.js] 527 KiB {0} [built] +[./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 472 bytes {0} [built] +[./node_modules/webpack/buildin/module.js] (webpack)/buildin/module.js 497 bytes {0} [built] +i ?wdm?: Failed to compile. +``` + +## Root Cause + +Webpack is trying to build the React SDK library into the server side, which will cause errors since React SDK is designed to run only on browser side. + +Answer +To resolve the issue, open `webpack.config.js` file, locate the resolve section, make sure the `mainFields` entry contain `browser` similar to this example: +```json +resolve: { + extensions: ['.js', '.json', '.ts', '.tsx'], + mainFields: ['browser', 'main', 'module'] + }, +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-is-it-possible-to-get-treatments-outside-the-components.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-is-it-possible-to-get-treatments-outside-the-components.md new file mode 100644 index 00000000000..22d064d4359 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-is-it-possible-to-get-treatments-outside-the-components.md @@ -0,0 +1,35 @@ +--- +title: "React SDK: Is it possible to get treatments outside the Components?" +sidebar_label: "React SDK: Is it possible to get treatments outside the Components?" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 5 +--- + +

+ +

+ +## Question + +Using the React SDK, is it possible to get Split treatments through Javascript code in addition to using the SDK React components? + +## Answer + +Yes, it is possible. The React SDK is created on top of the same Javascript SKD core, so it supports all the objects and functions provided in it. + +The code below can be used to get treatment using the same factory object created initially: +```javascript +import { SplitSdk } from "@splitsoftware/splitio-react" + +const splitFactory = SplitSdk({ + core: { + authorizationKey: 'YOUR_BROWSER_API_KEY', + key: 'key' + } +}) + +// create new client object +const client = splitFactory.client("userId"); +const treatment = client.getTreatment("splitName"); +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-istimeout-prop-not-returning-true-when-react-sdk-times-out.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-istimeout-prop-not-returning-true-when-react-sdk-times-out.md new file mode 100644 index 00000000000..ba1f35c119b --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-istimeout-prop-not-returning-true-when-react-sdk-times-out.md @@ -0,0 +1,48 @@ +--- +title: "isTimeout prop not returning true when React SDK times out" +sidebar_label: "isTimeout prop not returning true when React SDK times out" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 3 +--- + +

+ +

+ +## Issue + +When using Split React SDK, it is recommended to check if the SDK has timed out within a specific timeout before it finish downloading the cache and signal its ready. + +For the example below, the code does not display the message when SDK has timed-out: +```javascript +import { useContext } from 'react'; +import { SplitContext } from "@splitsoftware/splitio-react"; +const MyComponent = () => { + const { isReady, isTimedout } = useContext(SplitContext); + return isTimedout ? +

SDK has Timedout

+ : + +} +``` + +## Root Cause + +When the SDK reaches the time out value that is specified in parameter below: +```javascript +startup.readyTimeout +Which by default is 10 seconds, the SDK_READY_TIMED_OUT event is fired only once. If the code that is using the isTimedoutprop is placed after the event has fired, it will not detect it. +``` + +## Solution +The SDK provide the `updateOnSdkTimedout` prop for this scenario, setting this prop to `true` will enforce always checking if the `SDK_READY_TIMED_OUT` has been fired in the past, the code below will resolve the issue: +```javascript + + +{({ isReady, isTimedout, hasTimedout, lastUpdate, treatments }) => { +// Do something with the treatments for the account traffic type. +}} + + +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-lazy-initialization-of-split-client.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-lazy-initialization-of-split-client.md new file mode 100644 index 00000000000..a86face1846 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-lazy-initialization-of-split-client.md @@ -0,0 +1,80 @@ +--- +title: "React SDK: Lazy initialization of Split client" +sidebar_label: "React SDK: Lazy initialization of Split client" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 12 +--- + +

+ +

+ +## Question + +When using React app, on initial load of a client-side application the Split key is not always directly available. The React SDK will initialize SplitFactory and useClient on the initial render, which means that with the current setup we have to initiate the Split client with a key that might not exist yet. + +## Answer + +React SDK allows multiple client objects using the same Factory instance. It is recommended to use a dummy user key for the initial render, then when the actual user key is available initialize a second client object with the correct user key. + +With the release of React SDK v1.2.0 this can be done properly via the SplitClient component or useClient hook. + +Using the Javascript SDK: + +```javascript +/* On initial load of a client-side application / +const config = { + core: { + authorizationKey: 'YOUR-API-KEY', + key: 'anonymous' + } +} +const factory = SplitFactory(config); +const client = factory.client(); +// attach a callback to run when the client with 'anonymous' key is ready +client.on(client.Events.SDK_READY, doSomething); +/ When you get a new user id, for instance, the id of a logged user */ +const loggedClient = factory.client('user_id'); +// attach a callback to run when the client with 'user_id' key is ready +loggedClient.on(loggedClient.Events.SDK_READY, doSomething); +``` + +Using React SDK with SplitClient component + +```javascript +import React, { useState, useEffect } from 'react'; +import ReactDOM from 'react-dom'; +import { SplitFactory, SplitClient, SplitTreatments } from '@splitsoftware/splitio-react'; +import './index.css'; +const sdkConfig = { + core: { + authorizationKey: 'YOUR-API-KEY', + key: 'anonymous', + } +} + +function App() { + // using 'anonymous' as initial userId + const [userId, setUserId] = useState(sdkConfig.core.key); + // update userId to 'loggedinId' after 3 seconds + useEffect(() => { + setTimeout(() => { setUserId('loggedinId'); }, 3000); + }, []) + return ( + + + + {({ treatments, isReady }) => { + return isReady ? +

Treatment for {userId} in {featureName} is: {treatments[featureName].treatment}

: +

loading...

; // Render a spinner if the SDK is not ready yet + }} +
+
+
+ ); +} + +export default App; +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/redux-sdk-control-treatment-returned-when-sdk-is-initialized.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/redux-sdk-control-treatment-returned-when-sdk-is-initialized.md new file mode 100644 index 00000000000..2ba7d95f508 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/redux-sdk-control-treatment-returned-when-sdk-is-initialized.md @@ -0,0 +1,53 @@ +--- +title: "Redux SDK: Control treatment returned when SDK is initialized" +sidebar_label: "Redux SDK: Control treatment returned when SDK is initialized" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 1 +--- + +

+ +

+ +## Issue + +Implementing the Redux SDK using isReady prop should guarantee correct treatment, however, There's a split second where 'isReady' is true and the treatment is control right after the SDK factory is initialized. The treatment flips quickly to on quickly. What is causing this flickering? Example code below: + +``` +export default function initialise() { + store.dispatch(initSplitSdk({ config: sdkBrowserConfig, onReady: onReadyCallback, onUpdate: onUpdateCallback })); +} function onReadyCallback() { + console.log("Split Ready..."); + store.dispatch(getTreatments({ splitNames: ['Show_league'] })); +... +} function onUpdateCallback() { + console.log("Split updated..."); + store.dispatch(getTreatments({ splitNames: ['Show_league'] })); +... +} +``` + +## Root Cause + +When the SDK initializes, it starts downloading the cache from Split cloud, during this time isReady is false, if we try fetching treatments at that point, we will get control. We also need to evaluate updating isReady flag to true once the SDK is ready asynchronously. + +## Solution + +The solution is to dispatch `getTreatments` actions immediately after the `initSplitSdk` action. `getTreatments` creates an async (Thunk) action that will evaluate feature flags when the SDK is ready, and also on SDK updates if you set the `evalOnUpdate` param to true (it is false by default). This way the isReady flag will update together with the treatments values, in a single **action**. +In the first approach (dispatching the `getTreatments` action in `onReadyCallback`), there are two separate updates: one of the isReady flag, and a second one of the treatments values (after dispatching `getTreatment` action in the callback). + +For more details, check our public documentation, [here](https://help.split.io/hc/en-us/articles/360038851551-Redux-SDK#advanced-subscribe-to-events-and-changes). + +``` +export default function initialise() { + store.dispatch(initSplitSdk({ config: sdkBrowserConfig, onReady: onReadyCallback, onUpdate: onUpdateCallback })); + store.dispatch(getTreatments({ splitNames: ['Show_league'], evalOnUpdate: true })); +}function onReadyCallback() { + console.log("Split Ready..."); + ... +}function onUpdateCallback() { + console.log("Split updated..."); + ... +} +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/always-getting-control-treatments.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/always-getting-control-treatments.md new file mode 100644 index 00000000000..9e8d3260a5e --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/always-getting-control-treatments.md @@ -0,0 +1,41 @@ +--- +title: "General SDK: Always getting control treatments" +sidebar_label: "General SDK: Always getting control treatments" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 10 +--- + +

+ +

+ +## Problem + +When using SDK, control treatment is either always or very often returned from `getTreatment` call. + +## Root Cause + +When `getTreatment` call returns `control`, this means either: + +* The there is an issue with network connection to Split cloud and http calls are timing out. Enable the SDK debugging log file to verify if there are any network errors. +* The SDK is still downloading relevant feature flag definitions and Segments from Split cloud and still did not finish when `getTreatment` call is executed. + +## Solution + +The `control` treatment is most likely to return using the mobile SDKs; Javascript, Android and iOS. Simply because potentially the SDK runs on users' mobile devices which may have a slow network connection. + +That is why for these SDKs `getTreatment` should always be called when the SDK_READY events fires, which will ensure it's called after the SDK downloads all the information from Split cloud and avoid returning `control` treatments. + +``` +client.on(client.Event.SDK_READY, function() { + var treatment = client.getTreatment("SPLIT_NAME"); + if (treatment == "on") { + // insert code here to show on treatment + } else if (treatment == "off") { + // insert code here to show off treatment + } else { + // insert your control treatment code here + } +}); +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/error-gettreatment-you-passed-split-name-that-does-not-exist-in-this-environment.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/error-gettreatment-you-passed-split-name-that-does-not-exist-in-this-environment.md new file mode 100644 index 00000000000..2a565391085 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/error-gettreatment-you-passed-split-name-that-does-not-exist-in-this-environment.md @@ -0,0 +1,41 @@ +--- +title: "General SDK error, getTreatment: you passed \"SPLIT NAME\" that does not exist in this environment" +sidebar_label: "General SDK error, getTreatment: you passed \"SPLIT NAME\" that does not exist in this environment" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 8 +--- + +

+ +

+ +##Problem + +When using Split SDK and calling getTreatment for a list of feature flags names, there are lot of errors raised as below + +``` +admin 10 May 2019, 18:10:12 2019-05-10T17:10:12,445 ERROR [admin] [f0f338a964a0e3e1/07cfe07d08568096] [SplitClientImpl:256] - getTreatment: you passed "SPLIT NAME" that does not exist in this environment, please double check what Splits exist in the web console. +``` + +## Root Cause + +This error is part of the validation mechanism in the SDK, if the `getTreatment` call is passing a feature flag name that does not exist in the environment (for which the API key is used), this error will be thrown, since it's impossible to calculate the treatment at this point. + +## Solution + +Either make sure the feature flag names passed are part of the environment, or use the SDK Manager object to loop through the feature flag names that are added to the environment. This way we can avoid passing an incorrect flag name, as in the Java example below: + +``` +SplitFactory splitFactory = SplitFactoryBuilder.build("YOUR_API_KEY"); +// Some code +boolean CheckIfSplitExist(String splitName) { + List splitNames = splitFactory.manager().splitNames(); + for (int i = 0; i < splitNames.size(); i++) { + if (splitNames.get(i).equals(splitName)) + return true; + } + } + return false; +} +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/how-do-i-find-out-what-changed-in-an-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/how-do-i-find-out-what-changed-in-an-sdk.md new file mode 100644 index 00000000000..190176b434e --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/how-do-i-find-out-what-changed-in-an-sdk.md @@ -0,0 +1,21 @@ +--- +title: How do I find out what changed in an SDK? +sidebar_label: How do I find out what changed in an SDK? +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 11 +--- + +

+ +

+ +## Question + +How do I find out what changed when Split releases a new version of the SDK? + +### Answer + +For information about changes in the SDK over time you can start at [GitHub](https://github.com/splitio). + +Click on your clients of choice. Under CHANGES.txt, you'll find what is different and when the change was made. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/how-to-ensure-sdk-is-configured-to-handle-the-generated-impressions-and-events-load.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/how-to-ensure-sdk-is-configured-to-handle-the-generated-impressions-and-events-load.md new file mode 100644 index 00000000000..e1b2544d529 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/how-to-ensure-sdk-is-configured-to-handle-the-generated-impressions-and-events-load.md @@ -0,0 +1,53 @@ +--- +title: "General SDK: How to ensure SDK is configured to handle the generated impressions and events load?" +sidebar_label: "General SDK: How to ensure SDK is configured to handle the generated impressions and events load?" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 7 +--- + +

+ +

+ +By default, all Split SDKs have default configuration that allows them to process a heavy load of generated Impressions and Events. These config parameters values are documented in the help section of each SDK. + +For example, if we take a look at the [Java SDK](https://help.split.io/hc/en-us/articles/360020405151-Java-SDK), the Configuration section has the following parameters and their default values for posting impressions: + +``` +impressionsRefreshRate = 60 (seconds) +impressionsQueueSize = 30k +``` + +For the Impressions, the above settings means the SDK can handle up to 30k impressions (each impression is generated by one getTreatment call) every 60 seconds. + +If your code is generating more than 30k impressions per minute, then you will need to update these parameters. For example if you are generating 60k impressions per minute you could set: + +``` +impressionsRefreshRate = 20 +``` + +Or +``` +impressionsQueueSize = 70k +``` + +It's always advisable to provide some buffer for the SDK to handle the actual load. + +:::note +If the Queue size is reached, the SDK will attempt to post its contents regardless of when the impression post thread runs. + +The same concept is applied to the events created by using the SDK track() method. Here are the parameters corresponding to posting events: + +``` +eventFlushIntervalInMillis = 30000 (30 seconds) +eventsQueueSize = 500 +The values above will allow the Java SDK to handle up to 1000 events per minute. +``` +::: + +:::important +What happen if the SDK cannot keep up with the incoming Impressions and Events load? The SDK will post the Queue content when it becomes full. However, in case where the load is higher than it can handle, it will be constantly detecting the queue is full and posting impressions. Once the impressions are posted, then it will clear the queue. In the meantime, when new impressions are created, they are not stored in the queue (since its full), and thus these new impressions are lost. + +That is why it is important to verify these SDK configuration parameters values against your production Impressions and Events generation rate and make sure the SDK is configured to handle the load. +::: \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/how-to-use-split-sdks-with-split-proxy.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/how-to-use-split-sdks-with-split-proxy.md new file mode 100644 index 00000000000..76efc9fd320 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/how-to-use-split-sdks-with-split-proxy.md @@ -0,0 +1,105 @@ +--- +title: "General SDK: How to use Split SDKs with Split Proxy?" +sidebar_label: "General SDK: How to use Split SDKs with Split Proxy?" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 3 +--- + +

+ +

+ +## Question + +All Split SDKs support connecting to Split Proxy. + +What are the updates needed for each SDK to accomplish this? + +## Answer + +1. Make sure to obtain the Split Proxy URL +2. Refer to each SDK section below for the Configuration parameters needed, set the URL parameters to the full URL of the proxy location. + +### Javascript SDK +``` +core: { ... }, +urls: { + sdk: 'http://ProxyServerName:Port/api', + events: 'http://ProxyServerName:Port/api', + auth: 'https://ProxyServerName:Port/api', +}, +``` + +### iOS SDK +``` +let endpoints: ServiceEndpoints = ServiceEndpoints.builder() + .set(sdkEndpoint: "http://ProxyServerName:Port/api") + .set(eventsEndpoint: "http://ProxyServerName:Port/api") + .set(authServiceEndpoint: "http://ProxyServerName:Port/api") + .set(telemetryServiceEndpoint: "http://ProxyServerName:Port/api") + .build() + +let config = SplitClientConfig() +config.serviceEndpoints = endpoints +``` + +### Android SDK +``` +final ServiceEndpoints serviceEndpoints = ServiceEndpoints.builder() + .apiEndpoint("http://ProxyServerName:Port/api") + .eventsEndpoint("http://ProxyServerName:Port/api") + .sseAuthServiceEndpoint("http://ProxyServerName:Port/api") + .streamingServiceEndpoint("http://ProxyServerName:Port/api") + .telemetryServiceEndpoint("http://ProxyServerName:Port/api") + .build(); + +SplitClientConfig config = SplitClientConfig.builder() + .serviceEndpoints(serviceEndpoints) + .build(); +``` + +### Java SDK +``` +SplitClientConfig config = SplitClientConfig.builder() + .endpoint("http://ProxyServerName:Port", "http://ProxyServerName:Port") + .authServiceURL("http://ProxyServerName:Port") + .telemetryURL("http://ProxyServerName:Port") + .build(); +``` + +### Ruby SDK +``` +options = { + base_uri: "http://ProxyServerName:Port/api" + events_uri: "http://ProxyServerName:Port/api" + auth_service_url: "http://ProxyServerName:Port/api" + telemetry_service_url: 'http://ProxyServerName:Port/api', + streaming_service_url: 'http://ProxyServerName:Port/api' +} +``` + +### Python SDK +``` +config = {} +factory = get_factory('YOUR_API_KEY', config=config, sdk_api_base_url = 'http://ProxyServerName:Port/api', events_api_base_url = 'http://ProxyServerName:Port/api') +GO SDK + +cfg := conf.Default() +cfg.Advanced.AuthServiceURL = "http://ProxyServerName:Port/api" +cfg.Advanced.SdkURL = "http://ProxyServerName:Port/api" +cfg.Advanced.EventsURL = "http://ProxyServerName:Port/api" +cfg.Advanced.TelemetryServiceURL = "http://ProxyServerName:Port/api" +``` + +### .NET SDK +``` +var config = new ConfigurationOptions +{ + Endpoint = "http://ProxyServerName:Port", + EventsEndpoint = "http://ProxyServerName:Port", + AuthServiceURL = "http://ProxyServerName:Port/api/v2/auth",", + StreamingServiceURL = "http://ProxyServerName:Port/sse", + TelemetryServiceURL = "http://ProxyServerName:Port/api/v1" +}; +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/is-it-possible-to-call-getthreatment-function-without-passing-a-user-id.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/is-it-possible-to-call-getthreatment-function-without-passing-a-user-id.md new file mode 100644 index 00000000000..a379c5ba3c7 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/is-it-possible-to-call-getthreatment-function-without-passing-a-user-id.md @@ -0,0 +1,22 @@ +--- +title: Is it possible to call getTreatment() function without passing a user id? +sidebar_label: Is it possible to call getTreatment() function without passing a user id? +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 4 +--- + +

+ +

+ +## Question + +There are scenarios when there is a need to calculate treatment without specifying a user id, for example, when using a feature flag as a 100% feature toggle; either `"On"` or `"Off"`. Is there a way to omit the user id from the `getTreatment` call? + +## Answer + +The getTreatment function requires a customer id, which is usually a hash representation of a the current session's customer. The SDK uses the customer id when the feature flag includes percentage based targeting rules (for example 50% `"On"` and 50% `"Off"`). +In this case this is not relevant since we are assigning 100% of a single treatment. However, the SDK still requires the customer id to calculate the treatment. + +If the implementation will not use percentage based treatments, then apply a dummy customer id with any string value. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/is-it-possible-to-use-postman-to-calculate-a-treatment-for-a-feature-flag.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/is-it-possible-to-use-postman-to-calculate-a-treatment-for-a-feature-flag.md new file mode 100644 index 00000000000..4371e9f9ade --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/is-it-possible-to-use-postman-to-calculate-a-treatment-for-a-feature-flag.md @@ -0,0 +1,24 @@ +--- +title: Is it possible to use Postman to calculate a treatment for a given feature flag? +sidebar_label: Is it possible to use Postman to calculate a treatment for a given feature flag? +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 5 +--- + +

+ +

+ + +## Question + +When using Postman in developing environment, and knowing the SDK HTTP calls, can Postman be used as an alternative to the SDK library to calculate a treatment for a given feature flag? + +## Answer + +No. While Postman can use the same HTTP calls to download the feature flags definitions from the Split cloud, it needs to use the same Murmur hash that all SDKs use to assign a bucket (from 1 to 100) for a given user id, then apply the feature flag rules and conditions based on that bucket. + +This process is done by the SDK locally, and not through the Split cloud, which is why we need the SDK libraries. + +As a workaround, Split evaluator can be installed in the environment and Postman can use HTTP get requests to fetch a treatment for given feature flag and user id. The Split evaluator will perform the calculation and respond back to Postman with the corresponding treatment. Check out this [link](https://help.split.io/hc/en-us/articles/360020037072-Split-evaluator) for more info. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/isomorphic-javascript-wrapper-example.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/isomorphic-javascript-wrapper-example.md new file mode 100644 index 00000000000..e6c5b1c3e4d --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/isomorphic-javascript-wrapper-example.md @@ -0,0 +1,24 @@ +--- +title: Isomorphic JavaScript Wrapper Example +sidebar_label: Isomorphic JavaScript Wrapper Example +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 1 +--- + +

+ +

+ + +Isomorphic JavaScript, also known as universal JavaScript, emerged in response to the challenges posed by traditional client-server web architectures. It represents a paradigm shift in web development by enabling code execution on both the server and client sides, thereby promoting code reuse and seamless data synchronization. + +The concept gained traction in the early 2010s, with the rise of Node.js, which allowed JavaScript to run on the server. Isomorphic JavaScript frameworks, like Meteor and Next.js, gained popularity, as they facilitated server-side rendering, sharing codebases, and real-time data synchronization. + +By bridging the gap between server and client, isomorphic JavaScript significantly improved application performance, SEO, and overall user experience, becoming a fundamental approach in modern web development. + +In the [isomorphic_js_wrapper_demo code example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/Javascript-Isomorphic-Wrapper), we show that the Split JavaScript SDK is, indeed, isomorphic. + +The demo evaluates flags on both the server side and the client side using the same SDK Wrapper. This allows you to maintain only a single codebase for wrapping Split's SDK and ensures that you use the proper methods when on the server and the client with the same code. + +To see the full readme and code, visit the [Split Community's Split-SDKs-Examples](https://github.com/Split-Community/Split-SDKs-Examples/tree/main) repo. The direct link to this example is: https://github.com/Split-Community/Split-SDKs-Examples/tree/main/Javascript-Isomorphic-Wrapper \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/sdk-never-gets-ready-regardless-of-the-ready-timeout-value.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/sdk-never-gets-ready-regardless-of-the-ready-timeout-value.md new file mode 100644 index 00000000000..bccc30e0db8 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/sdk-never-gets-ready-regardless-of-the-ready-timeout-value.md @@ -0,0 +1,32 @@ +--- +title: "General SDK: SDK never gets ready, regardless of the ready timeout value" +sidebar_label: "General SDK: SDK never gets ready, regardless of the ready timeout value" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 9 +--- + +

+ +

+ +## Issue + +Split SDK never gets ready, regardless of how much the ready timeout value. + +## Root Cause + +There are several possible root causes for this issue: + +* If the SDK used is a server side type (Python, Ruby, GO, PHP, NodeJS or Java), and the API key used is Client-side type. The Split cloud service is expecting a specific call for Segment information which is different for Client-side vs Server-side API keys. +* Verify if there are large Segments in Split environment. Segments that contain tens of thousands of records will require a long time to be downloaded to the SDK cache. +* Verify network connection to sdk.split.io is fast. Use the command below to verify: +``` +curl sdk.split.io -s -o /dev/null -w "%{time_starttransfer}\n" +``` +The command will return the time it took the GET command to get a response. + +## Answer + +* Make sure to use the correct API key. +* Avoid using very large segments. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/sdk-readiness-always-times-out-when-running-in-kubernetes-and-istio-proxy.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/sdk-readiness-always-times-out-when-running-in-kubernetes-and-istio-proxy.md new file mode 100644 index 00000000000..89a0fe6190a --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/sdk-readiness-always-times-out-when-running-in-kubernetes-and-istio-proxy.md @@ -0,0 +1,41 @@ +--- +title: "General SDK: SDK Readiness always times out when running in Kubernetes and Istio proxy" +sidebar_label: "General SDK: SDK Readiness always times out when running in Kubernetes and Istio proxy" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 2 +--- + +

+ +

+ +## Issue + +Running an application that uses Split SDK in a Kubernetes container that is configured to use Istio proxy always results in SDK not ready exception. + +When enabling the SDK debug log files, it appears the SDK http calls are erroring out with **connection refused** error + +``` +DEBUG - 2021/09/13 12:48:07 [GET] https://sdk.split.io/api/splitChanges?since=-1 +DEBUG - 2021/09/13 12:48:07 Authorization [ApiKey]: xxxx...xxxx +DEBUG - 2021/09/13 12:48:07 Headers: map[Accept-Encoding:[gzip] Content-Type:[application/json] Splitsdkmachineip:[x.x.x.x] Splitsdkmachinename:[ip-x-x-x-x] Splitsdkversion:[go-6.0.2]] +ERROR - 2021/09/13 12:48:07 Error requesting data to API: https://sdk.split.io/api/splitChanges?since=-1 Get "https://sdk.split.io/api/splitChanges?since=-1": dial tcp 151.101.3.9:443: connect: connection refused +ERROR - 2021/09/13 12:48:07 Error fetching split changes Get "https://sdk.split.io/api/splitChanges?since=-1": dial tcp 151.101.3.9:443: connect: connection refused + ``` + +## Root Cause + +The SDK calls are being blocked by a proxy or firewall within the Kubernetes setup. Verify if the internet connection is enabled and the Kubernetes pod has access to sdk.split.io endpoint by ssh to the pod and running the curl below: +``` +curl -v https://sdk.split.io +``` + +If the error returned is 404, then the host is reachable. The issue might be with the Istio mesh. + +## Answer + +Make sure to let the application container run when the Istio sidecar proxy is ready. Add the following to the Istio config: +``` +--set meshConfig.defaultConfig.holdApplicationUntilProxyStarts=true + ``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/split-manager-returns-incomplete-list-of-feature-flags.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/split-manager-returns-incomplete-list-of-feature-flags.md new file mode 100644 index 00000000000..ad62123d592 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/split-manager-returns-incomplete-list-of-feature-flags.md @@ -0,0 +1,25 @@ +--- +title: "General SDK: Split Manager returns incomplete list of feature flags" +sidebar_label: "General SDK: Split Manager returns incomplete list of feature flags" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 6 +--- + +

+ +

+ +## Issue + +When using the SDK factory Manager object to fetch a list of information about feature flags, the list is incomplete and missing some flags that exist in the environment. + +## Root Cause + +Before using the factory Manager object, the SDK cache has to be completely downloaded and SDK status must be ready. If you attempt to fetch the list using the Manager object before the SDK is ready, a partial list is returned based on the contents of the cache. + +## Solution + +Make sure to verify the SDK is ready before using the factory Manager object, which is the same requirement when calling getTreatment. + +Please refer to each SDK's documentation for details on how to check that the SDK is ready. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/why-are-impressions-not-showing-in-split.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/why-are-impressions-not-showing-in-split.md new file mode 100644 index 00000000000..222f3263691 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/why-are-impressions-not-showing-in-split.md @@ -0,0 +1,37 @@ +--- +title: Why are impressions not showing in Split? +sidebar_label: Why are impressions not showing in Split? +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 12 +--- + +

+ +

+ +## Issue + +When using any SDK and calling the get treatment method, the call returns a correct treatment value. However, the impression is not getting sent to Split server and does not show up in Results page. + +## Root Cause + +There are few possible root causes: + +When using Redis and Split Synchronizer: + +* Synchronizer is not able to read the Impression key in Redis. Check for any errors in the Synchronizer debug log or Synchronizer admin console (http://[Synchronizer host]:3010//admin/dashboard) to determine root cause. +* Synchronizer is not keeping up with the impressions flowing from the SDK. Check the [KB article](https://help.split.io/hc/en-us/articles/360016299232-Configure-Split-Synchronizer-for-high-load-Impressions) for solution. + +When the SDK connects directly to Split Cloud: + +* All SDKs have a thread that runs frequently and checks the SDK cache for unpublished events and impressions. The frequency is controlled by the impressionsRefreshRate parameter for Impressions, and eventsPushRate for Events. If the SDK code exits while there is still unpublished cache, they will not be posted to Split Cloud. +* The Key Id (Customer, account, etc) used for the impression has more than 250 characters. +* If the SDK is running in an application environment that does not support multi-threading (like Ruby Unicorn and Python gunicorn), then only the main thread will run to calculate the treatments, but the post impressions thread will not run. + +## Solution + +* For mobile SDKs (Android and iOS), use `client.Flush()` method which will post impressions on demand, the method can be used when the application is sent to the background. +* Make sure to call the `destroy()` for the client object before exiting the code. Please refer to each SDK language section in our SDK documentation for the method syntax. While the destroy() method will flush all the unpublished impressions and events, it is also recommended to add a delay or wait command for enough time to have the internal impression posting thread run. +* Make sure the Key Id string used in get treatment method is no longer than 250 characters. +* Verify the application server supports multi-threading. If it doesn't, install Synchronizer and Redis and configure the SDK to connect to Redis for cache management. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/why-is-the-sdk-making-hundreds-of-network-calls-without-using-gettreatment-or-track-methods.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/why-is-the-sdk-making-hundreds-of-network-calls-without-using-gettreatment-or-track-methods.md new file mode 100644 index 00000000000..6f2731000ea --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/why-is-the-sdk-making-hundreds-of-network-calls-without-using-gettreatment-or-track-methods.md @@ -0,0 +1,65 @@ +--- +title: Why is the SDK making hundreds of network calls without using getTreatment or track methods? +sidebar_label: Why is the SDK making hundreds of network calls without using getTreatment or track methods? +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 13 +--- + +

+ +

+ +## Problem + +Using any Split SDK library, the Split library is making hundreds of network calls to split.io without using getTreatment or track methods + +## Root Cause + +If Splitio library is encapsulated in a class, and if every time the client object is needed, a new instance of factory and client objects are created, then all these objects will remain live in the memory and continue to perform synching the feature flag and segment changes with split.io. + +Here is a JavaScript SDK example of such code: + +```javascript +class SplitIO { + constructor() { + this.factory = splitio({ + core: { + authorizationKey: 'xxxx', + key: 'CUSTOMER_ID', + trafficType: 'client' + + } + }); + this.client = this.factory.client(); + } +} +mySplit = new SplitIO(); +mySplit2 = new SplitIO(); +mySplit3 = new SplitIO(); +``` + +## Solution + +We always recommend using a singleton factory object, and one client object especially if we are using only one traffic type and customer id. If we need to change either, then its recommended to initiate the client object only, as in the example below: + +```javascript +class SplitIO { + constructor() { + this.factory = splitio({ + core: { + authorizationKey: 'xxxx', + key: 'CUSTOMER_ID', + trafficType: 'client' + + } + }); + } + createClient(key, trafficType) { + return this.factory.client(key, trafficType); + } +} +mySplit = new SplitIO(); +client1 = mySplit.createClient(myKey, myTrafficType); +client2 = mySplit.createClient(myKey2, myTrafficType2); +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/_category_.json b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/_category_.json index 293b8480998..236f6b9fe48 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/_category_.json +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/_category_.json @@ -3,5 +3,5 @@ "collapsible": "true", "collapsed": "true", "className": "red", - "position": 8 + "position": 13 } \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/how-to-deploy-fme-synchronizer-docker-container-in-aws.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/how-to-deploy-fme-synchronizer-docker-container-in-aws.md new file mode 100644 index 00000000000..45f299b9a1f --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/how-to-deploy-fme-synchronizer-docker-container-in-aws.md @@ -0,0 +1,124 @@ +--- +title: How to deploy Synchronizer Docker container in Amazon AWS? +sidebar_label: How to deploy Synchronizer Docker container in Amazon AWS? +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 5 +--- + +

+ +

+ +## Question + +How to deploy Synchronizer Docker container in AWS ECS service + +## Answer + +Follow the steps below to deploy Synchronizer container in AWS ECS: + +1. First we need to create a repository in AWS ECR and push the docker image + +![](https://help.split.io/hc/article_attachments/360037885532) + +2. Provide a name for your new repository + +![](https://help.split.io/hc/article_attachments/4411390397453) + +3. Once the repository is created, we are ready to push the docker image, go back to repositories list to grab your repository unique URL, we use it later. + +![](https://help.split.io/hc/article_attachments/360037887352) + +4. Open a terminal or command line window, make sure you have AWS CLI installed and run the following command to login. If the command failed, please check AWS help pages. +``` +$(aws ecr get-login --no-include-email --region us-east-2) +``` + +5. Make sure you have Docker installed, then download the Split Synchronizer Docker image locally: +``` +docker pull splitsoftware/split-synchronizer +``` + +6. Run the command below to get the docker image id: +``` +docker images +REPOSITORY TAG IMAGE ID CREATED SIZE +splitsoftware/split-synchronizer latest 3179320c768e 3 weeks ago 941MB +``` + +7. Use the image id to tag it with the AWS repository URL copied previously: +``` + docker tag 3179320c768e 082XXXXX925.dkr.ecr.us-east-2.amazonaws.com/splitsync +``` + +8. Now push the image to AWS repository: +``` +docker push 082XXXXX925.dkr.ecr.us-east-2.amazonaws.com/splitsync +``` + +9. The image now will show up in AWS repository UI + +![](https://help.split.io/hc/article_attachments/360037892292) + +10. Next step is to create a cluster. Click on Amazon ECS Clusters link and click "Create + +![](https://help.split.io/hc/article_attachments/360037893392) + +11. Select cluster type. In this example we use AWS Fargate. + +![](https://help.split.io/hc/article_attachments/360037879751) + +![](https://help.split.io/hc/article_attachments/360037880451) + +12. Once the cluster is created, we need to create tasks. Click on ECS service and click Task Definitions. + +![](https://help.split.io/hc/article_attachments/360029344151) + +13. In the Task Definition page, click Create new Task Definition button and select the launch type based on your requirements. + +![](https://help.split.io/hc/article_attachments/360029344831) + +14. In the "Configure task and container definitions" page, under "Task Size", makes sure to specify the task memory and CPU. In this example, we set the memory to 2GB and 1 virtual CPU. + +![](https://help.split.io/hc/article_attachments/360029344412) + +15. Click "Add Container" button and specify Container name and set Image to the AWS container URL you just created. +``` +082XXXXX925.dkr.ecr.us-east-2.amazonaws.com/splitsync +``` + +16. Add the following port mappings: +``` +3000, tcp +3010, tcp +``` + +![](https://help.split.io/hc/article_attachments/360037885491) + +17. Add the required environment variables to the Container as needed by your setup. Look up the "Docker Environment Variable" column in the documentation [configuration section](https://help.split.io/hc/en-us/articles/360019686092-Split-synchronizer#common-configuration-synchronizer-and-proxy-mode). In our example, we specified the following variables: +``` +SPLIT_SYNC_APIKEY +SPLIT_SYNC_REDIS_HOST +SPLIT_SYNC_REDIS_PORT +SPLIT_SYNC_REDIS_DB +SPLIT_SYNC_REDIS_PASS +SPLIT_SYNC_ADMIN_USER +SPLIT_SYNC_ADMIN_PASS +``` + +![](https://help.split.io/hc/article_attachments/360029345171) + +18. Click "Add" to create the container, then "Create" button to create the task. + +![](https://help.split.io/hc/article_attachments/360037898872) + +19. Once the task is created, it's ready to run. + +![](https://help.split.io/hc/article_attachments/360037899312) + +![](https://help.split.io/hc/article_attachments/360037886071) + +20. To verify Synchronizer is running successfully, click on the Task Id and click on Logs tab, the Synchronizer startup std output should show up. + +![](https://help.split.io/hc/article_attachments/360037886331) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/how-to-deploy-synchronizer-docker-container-in-heroku.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/how-to-deploy-synchronizer-docker-container-in-heroku.md new file mode 100644 index 00000000000..a4a922b7013 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/how-to-deploy-synchronizer-docker-container-in-heroku.md @@ -0,0 +1,147 @@ +--- +title: How to deploy Synchronizer Docker container in Heroku? +sidebar_label: How to deploy Synchronizer Docker container in Heroku? +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 4 +--- + +

+ +

+ +## Question + +How to deploy Synchronizer Docker Container in heroku environment? + +## Answer + +Follow the steps below to deploy Synchronizer container in heroku + +1. Download the Synchronizer source code from the [git repository](https://github.com/splitio/split-synchronizer). + +2. Unzip the source code in a new folder (for example: mysync), then open terminal and cd to the folder. Run the command `heroku create`. This will create the heroku application, you will see a response similar to this: +``` +Creating app... done, ⬢ secret-anchorage-16496 +https://secret-anchorage-16496.herokuapp.com/ | https://git.heroku.com/secret-anchorage-16496.git +``` + +3. The next step is to set the image as container, then add the go language pack. +``` +heroku stack:set container +heroku buildpacks:set heroku/go +``` + +4. Open the existing Dockerfile and replace the last line +``` +ENTRYPOINT ["sh", "./entrypoint.sh"] +``` +with +``` +CMD ["sh", "./entrypoint.sh"] +``` +This is due to heroku supporting CMD only. + +5. Create new file named `heroku.yml` with the content below: +``` +build: + docker: + web: Dockerfile + worker: + dockerfile: Dockerfile + ``` + +6. Create new file name Procfile with the content below: +``` +worker: sh entrypoint.sh +``` + +7. To add the Docker environment variables, open the heroku.com page and click on your new app, click on "Settings" tab and add the environment variables below: +``` +SPLIT_SYNC_API_KEY +SPLIT_SYNC_REDIS_HOST +SPLIT_SYNC_REDIS_PORT +SPLIT_SYNC_REDIS_DB +SPLIT_SYNC_REDIS_PASS +``` + +8. To expose the Admin dashboard, we need to map it to the $PORT environment variable set by heroku, and overwrite the existing port used by synchronizer. Open the `entrypoint.sh` file and replace this line at the end: +``` +exec split-sync ${PARAMETERS} +``` +With: +``` +exec split-sync ${PARAMETERS} -sync-admin-port $PORT +``` + +9. Run the git commands below to push the image: +``` +git init +git add . +git commit +git push heroku master +``` + +10. Once the push is finished successfully, run the command `heroku logs` to verify if the synchronizer is running successfully. You should see content like below: +``` +2019-09-11T19:53:06.954882+00:00 app[worker.1]: __ ____ _ _ _ +2019-09-11T19:53:06.954887+00:00 app[worker.1]: / /__ / ___| _ __ | (_) |_ +2019-09-11T19:53:06.954890+00:00 app[worker.1]: / / \ \ \___ \| '_ \| | | __| +2019-09-11T19:53:06.954891+00:00 app[worker.1]: \ \ \ \ ___) | |_) | | | |_ +2019-09-11T19:53:06.954893+00:00 app[worker.1]: \_\ / / |____/| .__/|_|_|\__| +2019-09-11T19:53:06.954895+00:00 app[worker.1]: /_/ |_| +2019-09-11T19:53:06.954897+00:00 app[worker.1]: +2019-09-11T19:53:06.954899+00:00 app[worker.1]: +2019-09-11T19:53:06.954922+00:00 app[worker.1]: +2019-09-11T19:53:06.954924+00:00 app[worker.1]: Split Synchronizer - Version: 2.5.1 (2178c61) +2019-09-11T19:53:06.955154+00:00 app[worker.1]: Log file: /tmp/split-agent.log +``` + + 11. You can also view the admin dashboard from the URL `https://[heroku app name].herokuapp.com/admin/dashboard` + +:::info +When using Synchronizer as a proxy service, we have to assign the listener port to the $PORT environment variable created by heroku, which will be mapped to port 80 in heroku router. Follow the instructions below: + +a. Open `entrypoint.sh` file and scroll to the end, replace the line: +``` +exec split-sync ${PARAMETERS} +``` +With: +``` +exec split-sync ${PARAMETERS} -proxy-port $PORT +``` + +b. Deploy using web to open port 80 in heroku, change content of file Procfile with the content below: +``` +web: sh entrypoint.sh +``` + +c. Add the following environment variables in heroku UI with: +``` +SPLIT_SYNC_PROXY (value: on) +SPLIT_SYNC_PROXY_SDK_APIKEYS (value: any custom api key) +``` + +d. Push the new image using git: +``` +git add . +git commit +git push heroku master +``` + +e. On the SDK side, (for example Java SDK) use the heroku app URL for the endpoint in the config object: +```java +SplitClientConfig config = SplitClientConfig.builder() + .setBlockUntilReadyTimeout(5000) + .endpoint("http://[heroku app name].herokuapp.com", "http://[heroku app name].herokuapp.com") + .build(); +try { + splitFactory = SplitFactoryBuilder.build("custom api key", config); + client = this.splitFactory.client(); + client.blockUntilReady(); +} catch (Exception e) { + System.out.print("Exception: "+e.getMessage()); +} +``` +Since the default heroku deployment allows mapping only one port, which is used for the proxy listener service, the admin dashboard page will be unaccessible. +::: \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/how-to-inject-a-certificate-into-a-synchronizer-docker-image.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/how-to-inject-a-certificate-into-a-synchronizer-docker-image.md new file mode 100644 index 00000000000..55701b97e45 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/how-to-inject-a-certificate-into-a-synchronizer-docker-image.md @@ -0,0 +1,56 @@ +--- +title: How to inject a certificate into a Synchronizer Docker image? +sidebar_label: How to inject a certificate into a Synchronizer Docker image? +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 3 +--- + +

+ +

+ +## Question + +If the Synchronizer Docker container is running in a network that has a proxy using SSL for all traffic, the Synchronizer docker might not be able to authenticate the root certification, which will result in the error below when Synchronizer tries to connect to Split cloud to fetch the feature flags definitions: +``` +SPLITIO-AGENT | ERROR: 2020/08/19 14:42:51 fetchdataforproxy.go:209: Error fetching split changes Get https://sdk.split.io/api/splitChanges?since=-1: x509: certificate signed by unknown authority +``` +To resolve this issue, we need to inject the root certificate into the docker image. + +## Answer + +To accomplish this task, we will rebuild the Synchronizer docker image following the steps below: + +1. Download or clone the synchronizer public repo: +``` +git clone https://github.com/splitio/split-synchronizer +``` + +2. The clone command will create new folder `split-synchronizer`, `cd` to the folder and copy all the certifications used for the internal proxy, for example below, the root cert is `root.crt`, intermediate is intermediate.crt and the actual proxy cert is `proxy.pem`. +``` +cd split-synchronizer +cp [Path to your certs]/root.crt . +cp [Path to your certs]/intermediate.crt . +cp [Path to your certs]/proxy.pem +``` + +3. Open the file named `Dockerfile` located in `split-synchronizer` folder in any text editor and add these lines just before the `EXPOSE 3000 3010` line: +``` +COPY root.crt /etc/ssl/certs/root.crt +COPY intermediate.crt /etc/ssl/certs/intermediate.crt +COPY proxy.pem /etc/ssl/certs/proxy.pem +RUN cat /etc/ssl/certs/root.crt >> /etc/ssl/certs/ca-certificates.crt + +EXPOSE 3000 3010 +``` + +4. Save and close the file, now run the docker command below to build the new image: +``` +docker build --tag split-sync:latest . +``` + +5. Once the image is built successfully, you can run it using the command below to confirm Synchronizer is running successfully, the `http_proxy` parameter is optional. +``` +docker run --rm --name split-sync -p 3010:3010 --net="host" -e SPLIT_SYNC_API_KEY="SDK API KEY" -e SPLIT_SYNC_LOG_STDOUT="on" -e SPLIT_SYNC_LOG_DEBUG="true" -e SPLIT_SYNC_LOG_VERBOSE="true" -e SPLIT_SYNC_REDIS_HOST="Redis Host" -e SPLIT_SYNC_REDIS_PORT=6379 -e http_proxy="https://[internal proxy host]" -e https_proxy="https://[internal proxy host]" split-sync +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/http-error-when-using-proxy-mode.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/http-error-when-using-proxy-mode.md new file mode 100644 index 00000000000..5e67947a540 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/http-error-when-using-proxy-mode.md @@ -0,0 +1,35 @@ +--- +title: Synchronizer returns 500 HTTP error when used in proxy mode +sidebar_label: Synchronizer returns 500 HTTP error when used in proxy mode +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 2 +--- + +

+ +

+ +## Issue + +Synchronizer returns 500 HTTP error when used in proxy mode. + +Using Synchronizer in proxy mode, when trying to initialize a Split SDK factory connecting to the Synchronizer instance, the SDK never gets ready. + +## Root Cause + +Looking at the Synchronizer debug log below, looks like the Synchronizer's call to Split cloud is successful but the JSON structure returns empty feature flags names. + +While the HTTP call to Split cloud did not error out, the Synchronizer returns a 500 error to SDK, since getting an empty feature flag list means no flags where added to the environment corresponding to the SDK API key used. This will result in the current SDK session to be unable to calculate any treatments. + +``` +SPLITIO-AGENT - DEBUG - 2020/10/12 21:41:51 logger.go:35: GET |500| 285.71µs | 10.10.6.249 | /api/splitChanges +SPLITIO-AGENT - DEBUG - 2020/10/12 21:41:52 client.go:60: Authorization [ApiKey]: 1c9s...e19o +SPLITIO-AGENT - DEBUG - 2020/10/12 21:41:52 client.go:56: [GET] https://sdk.split.io/api/splitChanges?since=-1 +SPLITIO-AGENT - DEBUG - 2020/10/12 21:41:52 client.go:64: Headers: map[Accept-Encoding:[gzip] Content-Type:[application/json]] +SPLITIO-AGENT - VERBOSE - 2020/10/12 21:41:52 client.go:95: [RESPONSE_BODY] {"splits":[],"since":-1,"till":-1} [END_RESPONSE_BODY] +``` + +### Answer + +Make sure to add feature flags to the environment corresponding to the SDK API key used by Synchronizer, or use the correct SDK API key for the desired environment. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/no-impressions-sent-from-python-sdk-7.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/no-impressions-sent-from-python-sdk-7.md new file mode 100644 index 00000000000..aee48eb2646 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/no-impressions-sent-from-python-sdk-7.md @@ -0,0 +1,17 @@ +--- +title: No Impressions sent from Python SDK 7.x and Synchronizer 1.x +sidebar_label: No Impressions sent from Python SDK 7.x and Synchronizer 1.x +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 7 +--- + +

+ +

+ +## Issue +When using Synchronizer 1.x version and Python SDK 7.x version, the Python SDK is processing treatments correctly, Synchronizer does not report any errors, but no Impressions are sent to Split cloud. + +## Answer +As of Python SDK 7.0.0, design changes where made to match the enhancements made to Synchronizer 2.0 version. Thus, when Python SDK 7.x used, the Synchronizer used must be upgraded to 2.x version. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/post-method-404.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/post-method-404.md new file mode 100644 index 00000000000..bb5fb5e0dcb --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/post-method-404.md @@ -0,0 +1,49 @@ +--- +title: "Why do I see a \"POST method: Status Code: 404 - 404 Not Found\" Synchronizer error?" +sidebar_label: "Why do I see a \"POST method: Status Code: 404 - 404 Not Found\" Synchronizer error?" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 9 +--- + +

+ +

+ +## Issue + +After starting Split Synchronizer process (version 1.6.0 and above), Synchronizer debug log and Synchronizer admin dashboard show the error below on all its network Post calls: +"POST method: Status Code: 404 - 404 Not Found" + +![](https://help.split.io/hc/article_attachments/360013690471) + +## Root Cause + +The Error is due to incorrect Split API key passed to the Synchronizer. Which caused the Synchronizer inability to find to the Account in the Split cloud. + +## Solution + +First verify the API key used by Synchronizer is correct, Synchronizer API key must be SDK type, the API keys are viewed from Admin settings on the API keys page: +"https://app.split.io/org/[Your Account ID]/admin/apis" + +![](https://help.split.io/hc/article_attachments/360013671012) + +Second, make sure to pass the API key. There are many ways to do it: + +* Command line arguments: +``` +-api-key +``` + +* In a JSON file that Synchronizer uses for configuration. The `apiKey` property is the one that will be used to issue requests against Split Cloud. + +![](https://help.split.io/hc/article_attachments/360013671132) + +* Alternatively, if the Synchronizer is used only in proxy mode (not Redis), the "auth" section and "sdkAPIKeys" is used to allow setting custom apikeys for internal use, which allows the SDK to use the internal custom api key. + +![](https://help.split.io/hc/article_attachments/360013671492) + +* If the Synchronizer is running within the Split packaged docker image, make sure to use the parameter below: +``` +-e SPLIT_SYNC_API_KEY +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/running-evaluator-proxy-synchronizer-k8.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/running-evaluator-proxy-synchronizer-k8.md new file mode 100644 index 00000000000..a1728972902 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/running-evaluator-proxy-synchronizer-k8.md @@ -0,0 +1,15 @@ +--- +title: Running Split Evaluator, Split Proxy, or Split Synchronizer with Kubernetes +sidebar_label: Running Split Evaluator, Split Proxy, or Split Synchronizer with Kubernetes +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 1 +--- + +

+ +

+ +Split provides three containerized applications that can be run on your own infrastructure. These apps can handle specific use cases for feature flagging and experimentation with our SDKs and APIs. + +Learn more by reading the blog post, [Kubernetes and Split](https://www.split.io/blog/kubernetes-and-split/), which provides architectural diagrams and sample configuration files to run [Split Evaluator](https://help.split.io/hc/en-us/articles/360020037072-Split-Evaluator), [Split Proxy](https://help.split.io/hc/en-us/articles/4415960499213-Split-Proxy) and [Split Synchronizer](https://help.split.io/hc/en-us/articles/360019686092-Split-Synchronizer) in Kubernetes. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/sync-compatibility-matrix.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/sync-compatibility-matrix.md new file mode 100644 index 00000000000..47752037cd6 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/sync-compatibility-matrix.md @@ -0,0 +1,23 @@ +--- +title: Synchronizer Compatibility Matrix +sidebar_label: Synchronizer Compatibility Matrix +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 8 +--- + +

+ +

+ +| Language | Split Sync | Support | +| --- | --- | --- | +| Java | | Not implemented | +| JavaScript |1.x, 2.x JavaScript 9.x, 10.x | +| Ruby | 1.6, 1.7, 1.8, 2.x | Ruby 4.x, 5.x, 6.x (required for new 2.x features) | +| PHP | 1.6, 1.7, 1.8, 2.x | PHP 5.x | +| Python | 1.6, 1.7, 1.8, 2.x | Python 5.x | +| .NET/.NET Core | 1.6, 1.7, 1.8, 2.x | .NET/Core 2.1+, 3.x | +| Go Lang | 1.x, 2.x | GoLang 1.x | +|iOS | | NA | +| Android | | NA | \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/using-sdk-sync-gettreatment-control.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/using-sdk-sync-gettreatment-control.md new file mode 100644 index 00000000000..5d3b4bebda4 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/using-sdk-sync-gettreatment-control.md @@ -0,0 +1,55 @@ +--- +title: Using SDK with Synchronizer docker, getTreatment is always returning 'control' +sidebar_label: Using SDK with Synchronizer docker, getTreatment is always returning 'control' +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 6 +--- + +

+ +

+ +## Issue + +After installing the Split Synchronizer docker instance and running it successfully with Redis instance, then configuring SDK to use Redis, the getTreatment call is always returning 'control'. + +## Root Cause + +Synchronizer docker instance use a prefix for the Redis keys by default, if the SDK does not specify the same prefix in its redis configuration, it will not be able to read the Redis keys. + +## Solution + +1. Verify if the Synchronizer is using a prefix, run the commands below: +``` +redis-cli +Keys * +``` + +2. Verify if there is any text before "SPLITIO" in the key names, the example below suggest "myprefix" is used: +``` +127.0.0.1:6379> keys * + 1) "myprefix.SPLITIO.split.Split1" + 2) "myprefix.SPLITIO.splits.till" + 3) "myprefix.SPLITIO.split.Split2" + 4) "myprefix.SPLITIO.split.nico_test" + 5) "myprefix.SPLITIO.split.coach_matching_v1" + 6) "myprefix.SPLITIO.split.clients_on" + 7) "myprefix.SPLITIO.split.Split3" + 8) "myprefix.SPLITIO.split.sample_feature" + 9) "myprefix.SPLITIO.segments.registered" +10) "myprefix.SPLITIO.split.Demo_split" +11) "myprefix.SPLITIO.split.clients" +``` + +3. In your SDK code, add the configuration parameter for the redis-prefix, as shown in the example below for PHP SDK: +```php +from splitio import get_factory +config = { + 'redisHost' : 'localhost', + 'redisPort' : 6379, + 'redisDb' : 0, + 'redisPassword' : 'somePassword', + 'redisPrefix' : 'myprefix' +} +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/go-sdk-error-flushing-storage-queue.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/go-sdk-error-flushing-storage-queue.md new file mode 100644 index 00000000000..67ced08c7c5 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/go-sdk-error-flushing-storage-queue.md @@ -0,0 +1,34 @@ +--- +title: GO SDK Error flushing storage queue couldn't send message to task SubmitImpressions +sidebar_label: GO SDK Error flushing storage queue couldn't send message to task SubmitImpressions +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 6 +--- + +

+ +

+ +## Issue + +Using GO SDK, by default a thread will flush all current stored impressions in its cache every 30 seconds. However, there is a limit to the impression queued in the SDK's cache. If the queue is full, event IMPRESSIONS_FULL is fired and the SDK will attempt to post the impressions to clear the cache. When the process tries to flush all impressions, the error is logged: +``` +Error flushing storage queue couldn't send message to task SubmitImpressions +``` + +## Root cause + +The SDK is trying to send impressions at a higher rate than the posting thread is evicting them. + +## Answer + +To resolve the issue, follow these steps: + +1. Increase the size of the impressions queue by updating the `Advanced.ImpressionsQueueSize` parameter. Default is 10k, increasing it to 20k might improve results. +2. Increase the bulk size of the impressions post to Split servers by updating the `Advanced.ImpressionsBulkSize` parameter. Default is 5k. 10k would be a logical next step. +3. Decrease the period at which the SDK sends impressions to the Split servers by adjusting the `TaskPeriods.ImpressionSync` parameter. The default is 30 seconds which is on the low end if you're sending a huge number of impressions. Something along the lines of 5-10 seconds should help. + +:::note +These changes will slightly increase the memory usage of the SDK as well as the network traffic. +::: \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-exception-pkix-path-building-failed.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-exception-pkix-path-building-failed.md new file mode 100644 index 00000000000..9537b1d2c7b --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-exception-pkix-path-building-failed.md @@ -0,0 +1,41 @@ +--- +title: "Java SDK Exception: PKIX path building failed: unable to find valid certification path to requested target" +sidebar_label: "Java SDK Exception: PKIX path building failed: unable to find valid certification path to requested target" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 15 +--- + +

+ +

+ + +## Problem + +When Implementing Java SDK the exception below occurs initializing the SplitFactory object: +``` +RefreshableSplitFetcher failed: +Problem fetching splitChanges: +sun.security.validator.ValidatorException: +PKIX path building failed: +sun.security.provider.certpath.SunCertPathBuilderException: +unable to find valid certification path to requested target +``` + +## Root cause + +This exception means Java could not download the Split.io certificate, which will prevent the SSL connection to be established between the SDK and Split cloud. + +## Solution + +It's possible to install the Split.io certificate manually into any Java store the JVM is using. +Here are the steps to download the Split.io certificate and add it: +1. Run the command below to fetch the cert from sdk.split.io, re-run the command to fetch the cert from events.split.io + ``` +openssl s_client -showcerts -connect sdk.split.io:443 /dev/null|openssl x509 -outform PEM >splitsdkcert.pemopenssl s_client -showcerts -connect events.split.io:443 /dev/null|openssl x509 -outform PEM >spliteventscert.pem +``` +2. Run the keytool to import both certs into Java cacerts store, or specify any other ket store: + ``` +keytool -importcert -file splitsdkcert.pem -keystore [JAVA_HOME]/lib/security/cacerts -alias "splitsdkcert"keytool -importcert -file spliteventscert.pem -keystore [JAVA_HOME]/lib/security/cacerts -alias "spliteventscert" +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-fatal-alert-handshake-failure.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-fatal-alert-handshake-failure.md new file mode 100644 index 00000000000..d46378df17f --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-fatal-alert-handshake-failure.md @@ -0,0 +1,34 @@ +--- +title: "Java SDK error using JRE 6.x \"fatal alert: handshake_failure\"" +sidebar_label: "Java SDK error using JRE 6.x \"fatal alert: handshake_failure\"" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 12 +--- + +

+ +

+ + +## Issue + +Using Split Java SDK and JDK 1.6 (JRE 6.x), the following connection error to split.io is thrown: +``` +.RECV TLSv1 ALERT: fatal, handshake_failure + + handling exception: javax.net.ssl.SSLHandshakeException: Received fatal alert: + handshake_failure +``` + +## Root Cause + +Java 1.6 does support TLSv1 however, it does not support high strength Ciphers which are required by split.io security protocol. + +## Solution + +There are two solutions to this issue: + +* Upgrade your JDK to 1.7 or above (Java 7 or above). The newer versions will be packaged by default with the stronger ciphers needed. + +* Install the JCE (Java Cryptography Extension) from the JVM vendor for Java 6, which will provide the support for high strength ciphers. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-how-to-change-log-level.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-how-to-change-log-level.md new file mode 100644 index 00000000000..997303255ac --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-how-to-change-log-level.md @@ -0,0 +1,22 @@ +--- +title: Java SDK how to change log level +sidebar_label: Java SDK how to change log level +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 11 +--- + +

+ +

+ +## Question +When integrating Split Java SDK into a framework that uses Log4J, the SDK start logging lot of debugging lines, is it possible to change log level? + +## Answer +Split Java SDK will pick up the log4j.properties file used for the Java application. +To change the log level to error, add the following line to log4j.properties +``` +log4j.logger.split.org.apache = ERROR +log4j.logger.io.split = ERROR +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-how-to-deploy-in-aws-lambda.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-how-to-deploy-in-aws-lambda.md new file mode 100644 index 00000000000..73b0766628a --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-how-to-deploy-in-aws-lambda.md @@ -0,0 +1,101 @@ +--- +title: How to Deploy Java SDK in AWS Lambda +sidebar_label: How to Deploy Java SDK in AWS Lambda +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 8 +--- + +

+ +

+ +## Question + +How to deploy Java SDK code in AWS Lambda service. + +## Answer + +Prerequisites: + +1. We will use the Java SDK example code in this [KB Link](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/Java-SDK). Go ahead and download the example and make sure it runs successfully. +2. AWS Lambda only supports Java 8 as of writing this article, make sure to use JDK 1.8. + +Follow the steps below to run Java SDK as Lambda Function: + +1. Open the pom.xml file in the downloaded project and add the text below under `` block: + ``` + + com.amazonaws + aws-lambda-java-core + 1.0.0 + +``` +2. Open file SplitSDK_Sample.java and add the following imports in the beginning: + ```java +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +``` +3. Update the class SplitSDK_Sample definition with the line below. + ```java +public class SplitSDK_Sample implements RequestHandler { +``` +4. The code to implement Split SDK should be under the handleRequest function instead of main method remove the code under the main method and paste it under the handleRequest function as below: +```java +@Override +public String handleRequest(Object input, Context context) { + String myinput=input.toString(); + System.out.print("Input "+myinput+"\n\n"); + HashMap mapInput = (HashMap) input; + String userId = mapInput.get("key1"); + String treatment=""; + try { + MySplit split = new MySplit("API KEY"); + treatment = split.GetSplitTreatment(userId, "Split Name"); + System.out.print("Treatment: "+treatment+"\n\n"); + split.Destroy(); + } catch (Exception e) { + System.out.print("Exception: "+e.getMessage()); + } + return treatment; +} +``` +5. Build the Project in Eclipse using menu item **Project->Build Project**, verify no build errors. Make sure to build it as Package. + +6. In Eclipse Project Explorer view, right-click on your project and select **Export**. +![](https://help.split.io/hc/article_attachments/360039475392/Screen_Shot_2019-09-26_at_12.14.14_PM.png) + +7. Select **Runnable JAR** file option and click **Next**. + +![](https://help.split.io/hc/article_attachments/360039461751/Screen_Shot_2019-09-26_at_12.14.30_PM.png) + +8. Provide path and JAR file name, keep selection of library handling to Extract required libraries into generated JAR option and click **Finish** to generate the JAR file. + +![](https://help.split.io/hc/article_attachments/360039475872/Screen_Shot_2019-09-26_at_12.14.47_PM.png) + +9. Login to AWS, click on **Lambda->Functions** link and create new function and use the **Author from Scratch** option, select Java 8 Runtime option, type a function name and click **Create Function**. + +![](https://help.split.io/hc/article_attachments/360039461351/Screen_Shot_2019-09-26_at_12.09.14_PM.png) + +10. Under Function code section, click the **Upload** button and upload your exported JAR, then click the **Save** button at upper right corner. + +![](https://help.split.io/hc/article_attachments/360039475992/Screen_Shot_2019-09-26_at_12.21.10_PM.png) + +11. Next step is to configure test event, click on the drop down arrow and select **Configure test events** item. +![](https://help.split.io/hc/article_attachments/360039462191/Screen_Shot_2019-09-26_at_12.22.09_PM.png) + +12. Use the `Hello World` template to pass key dictionaries to your Lambda function, the user id used in GetTreatment call will be the value for key1, type a name for your event and click **Create**. + +![](https://help.split.io/hc/article_attachments/360039476332/Screen_Shot_2019-09-26_at_12.26.13_PM.png) + +13. Under the Function code section, overwrite the Handler edit box with the line below and click **Save**. +``` +sample.SplitSDK_Sample::handleRequest +``` +![](https://help.split.io/hc/article_attachments/4423286782989/Screen_Shot_2019-09-26_at_12.27.55_PM.png) + +14. The Lambda function is now ready to be used, click on **Test** button to run it, the expected output should be the treatment value, the log output will also show any logging info. + +![](https://help.split.io/hc/article_attachments/360039463251/Screen_Shot_2019-09-26_at_12.34.37_PM.png) + +[Download Example Project](https://drive.google.com/a/split.io/file/d/1iwl7u5ohAAx4PawuIw_gWb6kY_3Gfhs-/view?usp=sharing) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-is-there-a-jar-file.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-is-there-a-jar-file.md new file mode 100644 index 00000000000..bccc12e09f2 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-is-there-a-jar-file.md @@ -0,0 +1,24 @@ +--- +title: Is there a JAR file for Split Java SDK? +sidebar_label: Is there a JAR file for Split Java SDK? +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 9 +--- + +

+ +

+ +## Question + +Some Java Frameworks, like ColdFusion, allow third party JAR files to integrate with their code. How can we get a JAR file for Split Java SDK? + +## Answer + +Split Java SDK uses a Maven repository, which is why no JAR file is needed when using Maven engine to access the SDK and all its dependent libraries. +The JAR file can be downloaded from the Maven repository. Root access URL: +https://repo1.maven.org/maven2/io/split/client/java-client/ + +For example, the JAR file download URL for SDK version 4.2.1 is +https://repo1.maven.org/maven2/io/split/client/java-client/4.2.1/java-client-4.2.1.jar \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-time-out-error-nosuchmethoderror-google-common.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-time-out-error-nosuchmethoderror-google-common.md new file mode 100644 index 00000000000..9fb260c652d --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-time-out-error-nosuchmethoderror-google-common.md @@ -0,0 +1,35 @@ +--- +title: "Java SDK Time out Error: NoSuchMethodError: com.google.common.collect.Multisets.removeOccurrences" +sidebar_label: "Java SDK Time out Error: NoSuchMethodError: com.google.common.collect.Multisets.removeOccurrences" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 10 +--- + +

+ +

+ +## Issue + +Using Split Java SDK within a framework, SDK always times out. Log shows the error below: +``` +2602 [split-splitFetcher-0] ERROR io.split.engine.experiments.RefreshableSplitFetcher - RefreshableSplitFetcher failed: com.google.common.collect.Multisets.removeOccurrences(Lcom/google/common/collect/Multiset;Ljava/lang/Iterable;)Z +2603 [split-splitFetcher-0] DEBUG io.split.engine.experiments.RefreshableSplitFetcher - Reason: +java.lang.NoSuchMethodError: com.google.common.collect.Multisets.removeOccurrences(Lcom/google/common/collect/Multiset;Ljava/lang/Iterable;)Z + at io.split.engine.experiments.RefreshableSplitFetcher.runWithoutExceptionHandling(RefreshableSplitFetcher.java:214) + at io.split.engine.experiments.RefreshableSplitFetcher.run(RefreshableSplitFetcher.java:123) + at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:514) + at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:305) + at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:305) + at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1135) + at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) + at java.base/java.lang.Thread.run(Thread.java:844) +``` + +## Root Cause + +Split Java SDK uses Google Guava library, the error above will occur if the framework use Google Guava library below 19.0. + +## Solution +Upgrade Google Guava to 19.0 or above version. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/net-sdk-build-error-strongly-named-assembly.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/net-sdk-build-error-strongly-named-assembly.md new file mode 100644 index 00000000000..09c2bfcc360 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/net-sdk-build-error-strongly-named-assembly.md @@ -0,0 +1,31 @@ +--- +title: "Why do I see .NET SDK Build error \"Split 3.4.2.0 cannot be loaded since it needs a strongly-named assembly\"?" +sidebar_label: "Why do I see .NET SDK Build error \"Split 3.4.2.0 cannot be loaded since it needs a strongly-named assembly\"?" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 19 +--- + +

+ +

+ +## Problem + +In a .NET project that has signing enabled, after adding NET Split SDK, building the project will generate the warning: +``` +Referenced Assembly 'Splitio, Version=3.4.2.0, Culture=neutral, PublicKeyToken=null' does not have a strong name +``` + +When running the code, a run time error exception is triggered: +``` +Could not load file or assembly 'Splitio, Version=3.4.2.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. A strongly-named assembly is required. +``` + +## Root cause + +Split SDK versions below 3.4.4 have dependency libraries that are packaged without the signed version. In order to generate a signed Split SDK dll, all the dependency dll files must be signed first. + +## Solution + +We have released new Split SDK for .NET with the signed dependencies, please upgrade to latest version. Check our [SDK docs page](https://docs.split.io/docs/net-sdk-overview) for details on how to upgrade. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/net-xamarin-which-api-key.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/net-xamarin-which-api-key.md new file mode 100644 index 00000000000..2d2f600f400 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/net-xamarin-which-api-key.md @@ -0,0 +1,21 @@ +--- +title: Which API Key to use with .NET Xamarin project? +sidebar_label: Which API Key to use with .NET Xamarin project? +helpdocs_is_private: true +helpdocs_is_published: false +sidebar_position: 13 +--- + +

+ +

+ +## Question + +.NET development environment provides Xamarin project which allows .NET code to run on top of a container app in both iOS and Android platforms. + +Which API Key to use for .NET or .NET Core Split SDK when developing a Xamarin project? + +## Answer + +Use a server-side SDK API key. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-dependency-on-old-version-of-package-url-parse.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-dependency-on-old-version-of-package-url-parse.md new file mode 100644 index 00000000000..cc3acbc0cbf --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-dependency-on-old-version-of-package-url-parse.md @@ -0,0 +1,43 @@ +--- +title: "NodeJS SDK: Dependency on old version of package url-parse" +sidebar_label: "NodeJS SDK: Dependency on old version of package url-parse" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 1 +--- + +

+ +

+ +## Question + +Node.js SDK has a dependency on an old version of package url-parse (\<1.5.9), which is flagged as vulnerable in security scans. + +This package is in a dependency chain of eventsource package, this is the chain: +@splitsoftware/splitio > eventsource > original > url-parse + +## Answer + +To upgrade the url-parse package, simply run the command below for npm environment: +``` +npm audit fix +``` + +For yarn environment: +``` +npm_config_yes=true npx yarn-audit-fix +``` + +Or add a resolutions field in your app's package.jsonm as follows: + +```json +"resolutions": { + "url-parse": "1.5.10" +} +``` + +Then run: +``` +yarn upgrade +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-error-node_modules-has-no-exported-member-splitio.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-error-node_modules-has-no-exported-member-splitio.md new file mode 100644 index 00000000000..0253c442d6c --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-error-node_modules-has-no-exported-member-splitio.md @@ -0,0 +1,36 @@ +--- +title: "Node.js SDK error: \"/node_modules/@splitsoftware/splitio/types\"' has no exported member 'SplitIO'" +sidebar_label: "Node.js SDK error: \"/node_modules/@splitsoftware/splitio/types\"' has no exported member 'SplitIO'" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 5 +--- + +

+ +

+ +## Issue + +Using Node.js SDK, when trying to import SplitIO as a namespace in Typescript: +```javascript +import { SplitIo } from '@splitsoftware/splitio'; +``` + +The following error is thrown: +``` +/node_modules/@splitsoftware/splitio/types"' has no exported member 'SplitIO'. + ``` + +## Root cause + +TypeScript implicitly imports SplitIO namespace when doing `import { SplitFactory } from '@splitsoftware/splitio';`, and even the “typeRoots” config is not affecting it because the declaration file is included in the SDK package and the “types” field is properly configured. + +## Answer + +You can explicitly import the SplitIO namespace (for example, on modules/files where SplitFactory is not being imported). To achieve this, include the line: +``` +import SplitIO from '@splitsoftware/splitio/types/splitio'; +``` + +This requires including `"allowSyntheticDefaultImports": true` in tsconfig. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-how-to-deploy-in-aws-lambda.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-how-to-deploy-in-aws-lambda.md new file mode 100644 index 00000000000..9f0b16d9d0a --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-how-to-deploy-in-aws-lambda.md @@ -0,0 +1,82 @@ +--- +title: How to deploy NodeJS SDK in AWS Lambda +sidebar_label: How to deploy NodeJS SDK in AWS Lambda +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 7 +--- + +

+ +

+ +## Question + +How to deploy NodeJS SDK code in AWS Lambda service? + +## Answer + +Prerequisites: + +1. We will use similar code to the Javascript SDK example code in this [KB Link](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/Javascript-SDK). Go ahead and download the example and make sure it runs successfully. +2. AWS Lambda supports Node 10.x as of writing this article, make sure to use that version. + +Follow these steps to run NodeJS SDK as a Lambda Function: + +1. Create a node project and add the `index.js` file with the content below, make sure to replace the SDK API KEY, USER ID, and SPLIT NAME with corresponding value from your environment. + +```javascript +const SplitFactory = require('@splitsoftware/splitio').SplitFactory; + const SplitObj = SplitFactory({ + core: { + authorizationKey:'SDK API KEY' + }, + startup: { + readyTimeout :10 + }, + scheduler: { + impressionsRefreshRate: 1, + eventsPushRate: 2, + }, + debug: true + }); + +exports.handler = async (event) => { + const client = SplitObj.client(); + await client.ready(); + var p = new Promise(res => { + var treatment = client.getTreatment("USER ID", "SPLIT NAME"); + console.log("\ntreatment: "+treatment); + res(treatment); + }); + return await p; +}; +``` + +2. Add the dependency libraries for Split SDK, run the command below at the root of your project folder: + ``` +npm install --save @splitsoftware/splitio@10.16.0 +``` + +3. Zip up both `index.js` and `node_modules` folder into a new file named `function.zip`. + +4. Login to AWS, click on the **Lambda->Functions** link and create new function and use the **Author from scratch** option, select **Node 10.x** runtime option, type a function name and click **Create function**. + +![](https://help.split.io/hc/article_attachments/360041500292/Screen_Shot_2019-10-24_at_1.41.50_PM.png) + +5. Under Basic settings frame, set desired Memory and Timeout. In this example we set the memory to 256 MB and timeout to 1 minute. + +![](https://help.split.io/hc/article_attachments/360041501711/Screen_Shot_2019-10-24_at_3.16.58_PM.png) + +5. Using the AWS cli package, run the command below to upload your `function.zip` file to the newly created lambda function. + ``` +aws lambda update-function-code --function-name split_nodejs --zip-file fileb://function.zip +``` + +6. Configure test event: click on the drop down arrow and select **Configure test events** item, use the `Hello World` template to pass key dictionaries to your Lambda function, type a name for your event and click **Create**. + +![](https://help.split.io/hc/article_attachments/360039476332/Screen_Shot_2019-09-26_at_12.26.13_PM.png) + +7. The Lambda function is now ready to be used, click the **Test** button to run it, the expected output should be the treatment value, the log output will also show any logging info. + +![](https://help.split.io/hc/article_attachments/360041500532/Screen_Shot_2019-10-24_at_3.24.01_PM.png) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-localhost-mode-error-cannot-find-name-path.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-localhost-mode-error-cannot-find-name-path.md new file mode 100644 index 00000000000..ddb0ec59d51 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-localhost-mode-error-cannot-find-name-path.md @@ -0,0 +1,42 @@ +--- +title: "NodeJS SDK: While using Localhost mode, error generated: Cannot find name 'path'" +sidebar_label: "NodeJS SDK: While using Localhost mode, error generated: Cannot find name 'path'" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 4 +--- + +

+ +

+ +## Issue + +Using Node.js SDK, when trying to run the code below in Typescript file using Localhost mode: +```javascript +var factory = SplitFactory({ + core: { + authorizationKey: 'localhost' + }, + features: path.join(__dirname, '.split'), + scheduler: { + offlineRefreshRate: 15 // 15 sec + } +}); +``` + +The following error is thrown: +``` +Cannot find name 'path' + ``` + +## Root cause + +This is a node issue. Typescript needs typings for any module, except if that module is not written in typescript. + +## Answer + +You need to install the following package by running the command: +``` +npm i @types/node -D +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-using-gettreatment-in-localhost-mode-does-not-work-with-then-and-catch-blocks.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-using-gettreatment-in-localhost-mode-does-not-work-with-then-and-catch-blocks.md new file mode 100644 index 00000000000..897cc522725 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-using-gettreatment-in-localhost-mode-does-not-work-with-then-and-catch-blocks.md @@ -0,0 +1,70 @@ +--- +title: "NodeJS SDK: Using getTreatment() in localhost mode, does not work with then() and catch() blocks" +sidebar_label: "NodeJS SDK: Using getTreatment() in localhost mode, does not work with then() and catch() blocks" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 3 +--- + +

+ +

+ +## Issue + +When implementing NodeJS SDK with Redis storage, the getTreatment method is a wrapper for redis fetch call which returns a promise, which works fine with then() and catch() blocks. + +However, testing the SDK code in localhost mode below: + +```javascript +client + .getTreatment('user_id', 'my-feature-comming-from-redis') + .then(treatment => { + // do something with the treatment + }) + .catch(() => false) +``` + +It return the error below: +``` +splitClient.getTreatment(...).then is not a function + ``` + +## Root cause + +As mentioned when using redis storage getTreatment() function is a wrapper of a promise returned by redis library. So when no redis call is used in the localhost mode, the same code will error out. + +## Answer + +The SDK `getTreatment` method can be wrapped in async function, then use that function with `then()` and `catch()` blocks, as shown in sample below: + +```javascript +const path = require('path'); +const SplitFactory = require('@splitsoftware/splitio').SplitFactory; +async function createSplitClient() { + const SplitObj = SplitFactory({ + core: { + authorizationKey: 'localhost' + }, + startup: { + readyTimeout :10 + }, + features: path.join(__dirname, 'first.yaml'), + debug: true + }); + const client = SplitObj.client(); + await client.ready() + console.log("SDK is ready"); + return client +} +async function getSplitTreatment(userKey, splitName) { + let splitClient = await createSplitClient() + return await splitClient.getTreatment(userKey, splitName); +} +getSplitTreatment("user", "first_split").then((treatment)=> { + console.log("treatment: "+treatment) + }) +.catch(() => { + console.log("SDK exception") +}); +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/php-sdk-why-is-php-unable-to-write-impressions-to-redis.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/php-sdk-why-is-php-unable-to-write-impressions-to-redis.md new file mode 100644 index 00000000000..066f0c31bc1 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/php-sdk-why-is-php-unable-to-write-impressions-to-redis.md @@ -0,0 +1,53 @@ +--- +title: "Why is PHP unable to write impressions to Redis throwing error \"NOAUTH Authentication required\"?" +sidebar_label: "Why is PHP unable to write impressions to Redis throwing error \"NOAUTH Authentication required\"?" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 20 +--- + +

+ +

+ +## Question + +Why is PHP unable to write impressions to Redis throwing error "NOAUTH Authentication required"? + +## Problem + +Using PHP SDK, Redis and Split Synchronizer setup, when SDK code calls getTreatment function, an exception occurred: +``` +Fetching item ** SPLITIO.split.test ** from cache getTreatment method is throwing exceptions NOAUTH Authentication required. +``` + +## Root cause + +Redis instance requires authentication, the PHP code is missing the password parameter in the Redis configuration structure. +``` +$options = ['prefix' => '']; +$sdkConfig = array( + 'cache' => array('adapter' => 'predis', + 'parameters' => $parameters, + 'options' => $options + ) +); +``` + +## Solution + +Since the Split PHP SDK uses predis library, we can add the password parameter to the configuration structure: +``` +$options = [ + 'prefix' => '', + 'parameters' => [ + 'password' => 'REDISPASSWORD' + ], +]; +$sdkConfig = array( + 'cache' => array('adapter' => 'predis', + 'parameters' => $parameters, + 'options' => $options + ) +); +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/python-sdk-error-type-argument-1-must-be-string.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/python-sdk-error-type-argument-1-must-be-string.md new file mode 100644 index 00000000000..a19cc089a41 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/python-sdk-error-type-argument-1-must-be-string.md @@ -0,0 +1,41 @@ +--- +title: "What is the Python SDK error: \"type() argument 1 must be string, not unicode\"?" +sidebar_label: "What is the Python SDK error: \"type() argument 1 must be string, not unicode\"?" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 18 +--- + +

+ +

+ +## Issue +When initializing the SDK factory object in Python, this exception occurs: +``` + from splitio import get_factory + File "build/bdist.macosx-10.13-intel/egg/splitio/__init__.py", line 4, in + File "build/bdist.macosx-10.13-intel/egg/splitio/factories.py", line 4, in + File "build/bdist.macosx-10.13-intel/egg/splitio/clients.py", line 8, in + File "build/bdist.macosx-10.13-intel/egg/splitio/splitters.py", line 6, in + File "build/bdist.macosx-10.13-intel/egg/splitio/hashfns/__init__.py", line 9, in + File "build/bdist.macosx-10.13-intel/egg/splitio/splits.py", line 16, in + File "build/bdist.macosx-10.13-intel/egg/splitio/matchers.py", line 15, in + File "/Library/Python/2.7/site-packages/enum/__init__.py", line 326, in __call__ + return cls._create_(value, names, module=module, type=type) + File "/Library/Python/2.7/site-packages/enum/__init__.py", line 434, in _create_ + enum_class = metacls.__new__(metacls, class_name, bases, classdict) + File "/Library/Python/2.7/site-packages/enum/__init__.py", line 188, in __new__ + enum_class = super(EnumMeta, metacls).__new__(metacls, cls, bases, classdict) +TypeError: type() argument 1 must be string, not unicode +type() argument 1 must be string, not unicode +``` + +## Root cause +The Python Split SDK requires enum34 library version 1.1.5 or above, if a lower version of enum34 installed (for example 1.0.x), or the environment is forced to use this version, the exception above is thrown when initializing SDK factory object. + +## Solution +Upgrade enum34 to 1.1.5 or above using pip command. As of this article publishing date, the latest version is 1.1.6. +``` +sudo pip install enum34 --upgrade +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-close_wait-tcp-connections-in-puma.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-close_wait-tcp-connections-in-puma.md new file mode 100644 index 00000000000..b19cc1ad53d --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-close_wait-tcp-connections-in-puma.md @@ -0,0 +1,34 @@ +--- +title: "Ruby SDK: Why do CLOSE_WAIT TCP connections in Puma not go down as expected?" +sidebar_label: "Ruby SDK: Why do CLOSE_WAIT TCP connections in Puma not go down as expected?" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 17 +--- + +

+ +

+ + +## Issue + +Using Ruby SDK in Puma or Unicorn cluster mode, with multiple workers of one thread each, as the SDK is sending treatment events, CLOSE_WAIT TCP connections usually increase. This can be detected using the command: +``` +lsof -l | grep CLOSE_WAIT | wc -l +``` + +However, when no SDK treatment calls are placed, the CLOSE_WAIT TCP connections count does not go down as expected. + +## Root cause + +This might be caused by SDK threads not terminating properly, which will keep the client connection waiting for the server to send the final ACK signal. + +## Solution + +Puma will spawn new process for every group of incoming requests. To terminate all running threads before Puma closes the process, add the following code in config/puma.rb: +``` +before_fork do +$split_factory.instance_variable_get(:@config).threads.each { |_, t| t.exit } +end +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-error-uninitialized-constant.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-error-uninitialized-constant.md new file mode 100644 index 00000000000..dfa18b7ae45 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-error-uninitialized-constant.md @@ -0,0 +1,29 @@ +--- +title: "Ruby SDK Error: uninitialized constant error caused by 'Process::RLIMIT_NOFILE' in lib/net/http/persistent.rb" +sidebar_label: "Ruby SDK Error: uninitialized constant error. caused by 'Process::RLIMIT_NOFILE' in lib/net/http/persistent.rb" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 14 +--- + +

+ +

+ +## Problem +When using Split Ruby SDK in Windows Platform, initializing the Split factory object causes the error: +``` +uninitialized constant error. caused by 'Process::RLIMIT_NOFILE' in lib/net/http/persistent.rb +``` + +## Root Cause + +This issue is related to net-http-persistent 3.0 library in Windows OS. This is a dependent library that the SDK uses. The library gets installed as a dependency when installing the SDK gem. + +## Solution + +The Split SDK works fine with a slightly lower version of net-http-persistent (2.9.4), use the commands below to downgrade it: +``` +gem uninstall net-http-persistent +gem install net-http-persistent -v '2.9.4' +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-example-using-sdk-with-rails-and-sidekiq-service.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-example-using-sdk-with-rails-and-sidekiq-service.md new file mode 100644 index 00000000000..73bc9259478 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-example-using-sdk-with-rails-and-sidekiq-service.md @@ -0,0 +1,31 @@ +--- +title: "Ruby SDK: Example using Split SDK with Rails and Sidekiq service" +sidebar_label: "Ruby SDK: Example using Split SDK with Rails and Sidekiq service" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 2 +--- + +

+ +

+ +Example: Basic example to use Split Ruby SDK in Rails and Sidekiq service. + +Environment: + +Ruby 2.7.5 + +Steps to use: + +1. Example is in the repo link: https://github.com/sanzmauro/poc-test-split-io/tree/split-with-sidekiq, follow the readme instructions. +2. Run Sidekiq + ``` +bundle exec sidekiq -e ${RACK_ENV:-development} -r ./sidekiq/config/application.rb -C ./sidekiq/config/sidekiq.yml +``` +3. Run rails + ``` +rails s +``` +4. Request: `http://localhost:3030/test_global_feature_flag?key=&split_name=` +5. You will see in the sidekiq console, the result of the evaluations. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-upgrading-from-4-to-5-plus.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-upgrading-from-4-to-5-plus.md new file mode 100644 index 00000000000..820d4951a11 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-upgrading-from-4-to-5-plus.md @@ -0,0 +1,29 @@ +--- +title: "Ruby SDK: Upgrading from 4.x to 5.x and above" +sidebar_label: "Ruby SDK: Upgrading from 4.x to 5.x and above" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 16 +--- + +

+ +

+ +## Issue + +Under the hood, Split SDK has a hashing algorithm that divides users across treatments. For example, given a 50/50 split between two treatments (e.g., on and off), the hashing algorithm decides which user is in the on treatment and which one is in the off treatment. + +Split has used two hashing algorithms: +* Legacy Hash (or Algorithm 1). This is simple implementation that is optimized for speed, but suffers from uneven distributions when you have < 100 users. +* Murmur Hash (or Algorithm 2). This is an industry standard implementation that is both fast and gives even distribution whether evaluating 10, 100, or a 1M users. + +Ruby SDK versions 4.x and below, had an issue which resulted in any feature flag that was meant to use the Murmur hash would instead use the Legacy hash. This leads to the following problem: If you are using SDKs from multiple languages, Ruby would not be consistent with other languages. + +Version 5.0.0 and above of the Ruby SDK fixes the issue. However, as noted above, as you upgrade the Ruby SDK, you need to account for any active feature flags. + +For any feature flags that were in the process of ramp (meaning some users are being shown one treatment and others are being shown the other treatment), once you upgrade the SDK, the hashing algorithm will change, which means the users may shift from one treatment to another. + +## Recommendation + +In a perfect situation, the new SDK Ruby is applied when all experiments are in 100% distribution, if this is not possible, the recommendation is to create a new version for such current running feature flags to reset the metrics calculation, potentially this will give existing users different treatments, however, they will not be excluded from metric calculation since no treatments changes are recorded within the same version. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-daemon-splitd.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-daemon-splitd.md index 91215790af1..e5defabef05 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-daemon-splitd.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-daemon-splitd.md @@ -3,6 +3,7 @@ title: Split Daemon (splitd) sidebar_label: Split Daemon (splitd) helpdocs_is_private: false helpdocs_is_published: true +sidebar_position: 1 ---

diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-evaluator.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-evaluator.md index ce7a3b64b11..4015f3f1115 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-evaluator.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-evaluator.md @@ -3,6 +3,7 @@ title: Split Evaluator sidebar_label: Split Evaluator helpdocs_is_private: false helpdocs_is_published: true +sidebar_position: 2 ---

diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-javascript-synchronizer-tools.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-javascript-synchronizer-tools.md index e91d1637fc8..2a5d0ce77ef 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-javascript-synchronizer-tools.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-javascript-synchronizer-tools.md @@ -3,6 +3,7 @@ title: Split JavaScript synchronizer tools sidebar_label: Split JavaScript synchronizer tools helpdocs_is_private: false helpdocs_is_published: true +sidebar_position: 5 ---

diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-proxy.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-proxy.md index cc0ec78f8d9..f4a9a89599e 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-proxy.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-proxy.md @@ -3,6 +3,7 @@ title: Split Proxy sidebar_label: Split Proxy helpdocs_is_private: false helpdocs_is_published: true +sidebar_position: 3 ---

diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-synchronizer.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-synchronizer.md index b57bc849805..dde1502cab4 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-synchronizer.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-synchronizer.md @@ -3,6 +3,7 @@ title: Split Synchronizer sidebar_label: Split Synchronizer helpdocs_is_private: false helpdocs_is_published: true +sidebar_position: 4 ---

diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/go-app.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/go-app.md new file mode 100644 index 00000000000..5bcaf5c4884 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/go-app.md @@ -0,0 +1,13 @@ +--- +title: Go App Project using Split SDK example +sidebar_label: Go App Project using Split SDK example +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 7 +--- + +

+ +

+ +[Go App Project using Split SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/Go-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/go-sdk-localhost-mode-yaml.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/go-sdk-localhost-mode-yaml.md new file mode 100644 index 00000000000..19959a12cfb --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/go-sdk-localhost-mode-yaml.md @@ -0,0 +1,13 @@ +--- +title: Go SDK App using Localhost mode with yaml file example +sidebar_label: Go SDK App using Localhost mode with yaml file example +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 5 +--- + +

+ +

+ +[Go SDK App using Localhost mode with yaml file](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/Go-SDK-with-localhost) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/java-app.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/java-app.md new file mode 100644 index 00000000000..9c7f10f19da --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/java-app.md @@ -0,0 +1,13 @@ +--- +title: Java App Project using Split SDK example +sidebar_label: Java App Project using Split SDK example +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 12 +--- + +

+ +

+ +[Java App Project using Split SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/Java-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/java-sdk-scala-sbt-cl.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/java-sdk-scala-sbt-cl.md new file mode 100644 index 00000000000..6c31308e0fe --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/java-sdk-scala-sbt-cl.md @@ -0,0 +1,13 @@ +--- +title: Java SDK Example using Scala SBT command line compiler +sidebar_label: Java SDK Example using Scala SBT command line compiler +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 4 +--- + +

+ +

+ +[Java SDK Example using Scala SBT command line compiler](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/java-sdk-with-scala) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/net-core-csharp-app.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/net-core-csharp-app.md new file mode 100644 index 00000000000..0128e4584d5 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/net-core-csharp-app.md @@ -0,0 +1,13 @@ +--- +title: .NET Core C# App Project using Split SDK example +sidebar_label: .NET Core C# App Project using Split SDK example +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 10 +--- + +

+ +

+ +[.NET Core C# App Project using Split SDK](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/net-core-CSharp-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/net-core-vb.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/net-core-vb.md new file mode 100644 index 00000000000..4ef4f69e2f9 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/net-core-vb.md @@ -0,0 +1,79 @@ +--- +title: .NET Core VB using Split SDK example +sidebar_label: .NET Core VB using Split SDK example +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 3 +--- + +

+ +

+ +Example: Basic code to use .NET Split SDK 6.0.1 + +Environment: + +* Visual Studio for Mac 8.4 +* NuGet 5.3 +* .NET Core SDK 2.1.301 +* Packages + * Common.Logging.NLog41 3.4.1 + * NLog.Config 4.6.8 + * NLog 4.6.8 + * NLog.Schema 4.6.8 + +How to use: + +* Update your relevant Split API key, user ID, and Split names in: + +``` +Imports System +Imports Splitio.Services.Client.Classes +Imports Common.Logging.Configuration +Imports NLog +Imports NLog.Config +Imports NLog.Targets + +Public Class Application + Public Shared Sub Main() + +' Enable debug Logging + Dim fileTarget = New FileTarget With { + .Name = "splitio", + .FileName = "splitio.log", + .ArchiveFileName = "splitio.{#}.log", + .LineEnding = LineEndingMode.CRLF, + .Layout = "${longdate} ${level: uppercase = true} ${logger} - ${message} - ${exception:format=tostring}", + .ConcurrentWrites = True, + .CreateDirs = True, + .ArchiveNumbering = ArchiveNumberingMode.DateAndSequence, + .ArchiveAboveSize = 200000000, + .ArchiveDateFormat = "yyyyMMdd", + .MaxArchiveFiles = 30 + } + Dim rule = New LoggingRule("*", LogLevel.Debug, fileTarget) + Dim config = New LoggingConfiguration() + config.AddTarget("splitio", fileTarget) + config.LoggingRules.Add(rule) + LogManager.Configuration = config + Dim properties As NameValueCollection = New NameValueCollection() + properties("configType") = "INLINE" + Common.Logging.LogManager.Adapter = New Common.Logging.NLog.NLogLoggerFactoryAdapter(properties) + +' Using the Split SDK + Dim splitConfig As ConfigurationOptions + splitConfig = New ConfigurationOptions() + Dim factory As SplitFactory + factory = New SplitFactory("API Key", splitConfig) + Dim client As SplitClient + client = factory.Client() + client.BlockUntilReady(10000) + System.Console.WriteLine("SDK is Ready") + + Dim treatment As String + treatment = client.GetTreatment("User ID","Split Name") + System.Console.WriteLine(treatment) + End Sub +End Class +``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/net-csharp.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/net-csharp.md new file mode 100644 index 00000000000..339f26df4c7 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/net-csharp.md @@ -0,0 +1,13 @@ +--- +title: .NET C# App Project using Split SDK example +sidebar_label: .NET C# App Project using Split SDK example +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 6 +--- + +

+ +

+ +[.NET C# App project using Split SDK](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/netCsharp-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/net-sdk-debug-logging.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/net-sdk-debug-logging.md new file mode 100644 index 00000000000..93f90744c4a --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/net-sdk-debug-logging.md @@ -0,0 +1,13 @@ +--- +title: ".NET SDK: Simple example to enable debug logging" +sidebar_label: ".NET SDK: Simple example to enable debug logging" +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 1 +--- + +

+ +

+ +[.NET SDK Simple example to enable debug logging](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/netSDK/logging) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/nodejs-vue-nuxt-ssr.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/nodejs-vue-nuxt-ssr.md new file mode 100644 index 00000000000..8999fe7d3c4 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/nodejs-vue-nuxt-ssr.md @@ -0,0 +1,13 @@ +--- +title: NodeJS Example using Vue Framework and Nuxt library with Server Side Rendering +sidebar_label: NodeJS Example using Vue Framework and Nuxt library with Server Side Rendering +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 2 +--- + +

+ +

+ +[NodeJS example using Vue Framework and Nuxt Library](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/NodeJS-withVue-Nuxt) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/php-app.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/php-app.md new file mode 100644 index 00000000000..dfe311624fc --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/php-app.md @@ -0,0 +1,13 @@ +--- +title: PHP App Project using Split SDK example +sidebar_label: PHP App Project using Split SDK example +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 8 +--- + +

+ +

+ +[PHP App Project using Split SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/PHP-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/python-app.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/python-app.md new file mode 100644 index 00000000000..544b3dcd8ba --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/python-app.md @@ -0,0 +1,13 @@ +--- +title: Python App Project using Split SDK example +sidebar_label: Python App Project using Split SDK example +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 11 +--- + +

+ +

+ +[Python App Project using Split SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/Python-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/python-django-uwsgi.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/python-django-uwsgi.md new file mode 100644 index 00000000000..fc871a47374 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/python-django-uwsgi.md @@ -0,0 +1,46 @@ +--- +title: Python Django App with uWSGI using Split SDK example +sidebar_label: Python Django App with uWSGI using Split SDK example +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 6 +--- + +

+ +

+ +Example: Basic code to use Python Split SDK `8.0.0` in Django and uWSGI environment + +Environment: + +* Python 2.7.15 +* uWSGI 2.0.18 + +* uwsgidecorators 1.1.0 + +* Django 1.8.11 + +How to use: + +* Class wrapper for SplitSDK is: + mysite/splitSample/MySplit.py + +* Update your relevant Split API Key, Track type and Split names in: + mysite/splitSample/views.py + +* Update the Split API key for spool service in + mysite/sdk_spool.py + +* Update the project and spool directories under parameters "chdir" and "spooler" in file: + mysite/mysite_uwsgi.ini + +* Run the following command to start the uwsgi web server: + uwsgi --ini mysite_uwsgi.ini --import sdk_spool.py + +* Access the web page using the following URL: + http://localhost:8000/splitSample + + + +[Download Link](https://drive.google.com/a/split.io/file/d/17zqwfkwlX4Y0dED8gzhxk1p21YbIaX-8/view?usp=sharing) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/ruby-app.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/ruby-app.md new file mode 100644 index 00000000000..eb23030dca1 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/ruby-app.md @@ -0,0 +1,13 @@ +--- +title: Ruby App Project using Split SDK example +sidebar_label: Ruby App Project using Split SDK example +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 9 +--- + +

+ +

+ +[Ruby App Project using Split SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/Ruby-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/ruby-on-rails-puma.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/ruby-on-rails-puma.md new file mode 100644 index 00000000000..e214b483b68 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/ruby-on-rails-puma.md @@ -0,0 +1,13 @@ +--- +title: Ruby On Rails with Puma App engine Project using Split SDK example +sidebar_label: Ruby On Rails with Puma App engine Project using Split SDK example +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 13 +--- + +

+ +

+ +[Ruby On Rails with Puma App engine Project using Split SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/Ruby-on-rail-Puma-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/ruby-sdk-rails-caching.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/ruby-sdk-rails-caching.md new file mode 100644 index 00000000000..67f1c4ad858 --- /dev/null +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/ruby-sdk-rails-caching.md @@ -0,0 +1,69 @@ +--- +title: Ruby SDK and Rails caching +sidebar_label: Ruby SDK and Rails caching +helpdocs_is_private: false +helpdocs_is_published: true +sidebar_position: 14 +--- + +

+ +

+ +## Question +How can the Split SDK integrate with a Rails application that works with full page caching? + +### Environment +We created a demo app to test Rails caching working with Split SDK. Rails Version: 5.0.7, Puma Version: 3.12.0 (standalone). Ruby Version: 2.2.2-p95 + +We initialize the Split SDK as described in the [Split Documentation](https://docs.split.io/docs/ruby-sdk-overview#section-configuration) + +Initialization snippet (typically: config/initializers/split_client.rb) + +``` +factory = SplitIoClient::SplitFactoryBuilder.build('YOUR_API_KEY') Rails.configuration.split_client = factory.client +``` + +### [Page Caching](https://guides.rubyonrails.org/caching_with_rails.html#page-caching) Outcome +We implemented this by adding the actionpack-page_caching gem to the application. This type of caching creates a copy of the rendered page, which can be served instead of processing the template on each request by configuring the web server to do so. + +This approach doesn’t work with pages that need authentication, nor pages that use Split, because the first treatment that a user receives, will be displayed for all the subsequent users. + +## Proposals +### Option 1 - [Action caching](https://guides.rubyonrails.org/caching_with_rails.html#action-caching) +A refactor can be done to query the treatment in a non cached action, forcing the decision logic to run on each request (in our case, the index action), while caching the other actions that display the result of the treatments. + +![](https://help.split.io/hc/article_attachments/360008630892/image1.png) + +![](https://help.split.io/hc/article_attachments/360008630912/image2.gif) + +#### Additional details +This was implemented by adding actionpack-action_caching to the gem file. This type of caching allows caching an action in the controller, in our case we cached the index action. + +![](https://help.split.io/hc/article_attachments/360008630952/image3.png) + +This provides simple logic to show a message based on the treatment given to the user. + +![](https://help.split.io/hc/article_attachments/360008665111/image4.png) + +When the application was run, it loaded and called the SDK just once, then started showing the cached action. + +![](https://help.split.io/hc/article_attachments/360008630992/image5.png) + +In the browser: + +![](https://help.split.io/hc/article_attachments/360008631012/image6.png) + +### Option 2 - [Fragment caching](https://guides.rubyonrails.org/caching_with_rails.html#fragment-caching) + +Similarly, a refactor can be done caching only fragments of the code that won’t change despite the obtained treatment. + +![](https://help.split.io/hc/article_attachments/360008631032/image7.png) + +![](https://help.split.io/hc/article_attachments/360008631052/image8.gif) + +#### Additional details +This caching is included in Rails and allows to cache a fragment of a page. +We updated the previous view to show a cached fragment along with a non cached one, that’s recreated on each request. + +![](https://help.split.io/hc/article_attachments/360008631072/image9.png) \ No newline at end of file From b70522f7239f0b98b9916ac6493c206f75728554 Mon Sep 17 00:00:00 2001 From: lena sano Date: Sun, 9 Mar 2025 04:01:08 -0300 Subject: [PATCH 07/19] remove outdated Python Django example (c. 2019) --- .../python-django-uwsgi.md | 46 ------------------- 1 file changed, 46 deletions(-) delete mode 100644 docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/python-django-uwsgi.md diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/python-django-uwsgi.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/python-django-uwsgi.md deleted file mode 100644 index fc871a47374..00000000000 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/python-django-uwsgi.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: Python Django App with uWSGI using Split SDK example -sidebar_label: Python Django App with uWSGI using Split SDK example -helpdocs_is_private: false -helpdocs_is_published: true -sidebar_position: 6 ---- - -

- -

- -Example: Basic code to use Python Split SDK `8.0.0` in Django and uWSGI environment - -Environment: - -* Python 2.7.15 -* uWSGI 2.0.18 - -* uwsgidecorators 1.1.0 - -* Django 1.8.11 - -How to use: - -* Class wrapper for SplitSDK is: - mysite/splitSample/MySplit.py - -* Update your relevant Split API Key, Track type and Split names in: - mysite/splitSample/views.py - -* Update the Split API key for spool service in - mysite/sdk_spool.py - -* Update the project and spool directories under parameters "chdir" and "spooler" in file: - mysite/mysite_uwsgi.ini - -* Run the following command to start the uwsgi web server: - uwsgi --ini mysite_uwsgi.ini --import sdk_spool.py - -* Access the web page using the following URL: - http://localhost:8000/splitSample - - - -[Download Link](https://drive.google.com/a/split.io/file/d/17zqwfkwlX4Y0dED8gzhxk1p21YbIaX-8/view?usp=sharing) \ No newline at end of file From aa442911165ffc8766f9cead6a685fd6d9a5ea28 Mon Sep 17 00:00:00 2001 From: lena sano Date: Sun, 9 Mar 2025 05:00:27 -0300 Subject: [PATCH 08/19] NodeJS >> Node.js --- .../client-side-sdk-examples/react-native-app-nodejs.md | 6 +++--- ...hared-client-not-supported-by-the-storage-mechanism.md | 2 +- .../javascript-sdk-polimer-cli-enoent-error.md | 4 ++-- ...er-gets-ready-regardless-of-the-ready-timeout-value.md | 2 +- ...-sdk-dependency-on-old-version-of-package-url-parse.md | 4 ++-- .../nodejs-sdk-how-to-deploy-in-aws-lambda.md | 8 ++++---- ...dejs-sdk-localhost-mode-error-cannot-find-name-path.md | 4 ++-- ...lhost-mode-does-not-work-with-then-and-catch-blocks.md | 6 +++--- .../server-side-sdk-examples/nodejs-vue-nuxt-ssr.md | 6 +++--- 9 files changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-app-nodejs.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-app-nodejs.md index 7c1f75ef441..f66c35d7073 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-app-nodejs.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-app-nodejs.md @@ -1,6 +1,6 @@ --- -title: React Native App using Split NodeJS SDK example -sidebar_label: React Native App using Split NodeJS SDK example +title: React Native App using Split Node.js SDK example +sidebar_label: React Native App using Split Node.js SDK example helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 4 @@ -37,7 +37,7 @@ When running you should see a screen like the image below (taken from an Android ![](https://help.split.io/hc/article_attachments/360057415851/mobile_screenshot.png) ## Prerequisites -You'll need [NodeJS](https://nodejs.org/en/download/). We recommend that you use the latest LTS version. +You'll need [Node.js](https://nodejs.org/en/download/). We recommend that you use the latest LTS version. Second thing you'll need is to install [Expo-CLI](https://expo.io/) with the command `npm install -g expo-cli`. diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-error-shared-client-not-supported-by-the-storage-mechanism.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-error-shared-client-not-supported-by-the-storage-mechanism.md index 6d1d8742364..f6687598402 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-error-shared-client-not-supported-by-the-storage-mechanism.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-error-shared-client-not-supported-by-the-storage-mechanism.md @@ -18,7 +18,7 @@ Shared Client not supported by the storage mechanism. Create isolated instances ## Root cause -When using Jest for testing applications, Jest runs in NodeJS by default, and NodeJS does not support shared clients, which is why it detects the storage does not have that function. +When using Jest for testing applications, Jest runs in Node.js by default, and Node.js does not support shared clients, which is why it detects the storage does not have that function. It is not possible to overwrite that method from the outside. ## Solution diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-polimer-cli-enoent-error.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-polimer-cli-enoent-error.md index ca2bc4e887e..92c30d45000 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-polimer-cli-enoent-error.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-polimer-cli-enoent-error.md @@ -31,7 +31,7 @@ Error: ENOENT: no such file or directory, open '/Users/[USER_NAME]/projects/[PRO ## Answer The way Polymer is performing the build differs significantly from webpack and other bundlers that can recognize the right path for an isomorphic app. -Polymer is trying to load the Node code path of the SDK, which in turn tries to import the events module from NodeJS. +Polymer is trying to load the Node code path of the SDK, which in turn tries to import the events module from Node.js. Taking a look to what's on node_modules\@splitsoftware\splitio folder, you'll see a few package.json files with this format: @@ -42,6 +42,6 @@ Taking a look to what's on node_modules\@splitsoftware\splitio folder, you'll se } ``` -What we do there is tell NodeJS to just run the Node version of that module (the main field), while we tell bundlers to use the "Browser version". +What we do there is tell Node.js to just run the Node version of that module (the main field), while we tell bundlers to use the "Browser version". If the plan is to implement the JS SDK in both server and browser modes, make sure to set the browser and main values to the corresponding js code. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/sdk-never-gets-ready-regardless-of-the-ready-timeout-value.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/sdk-never-gets-ready-regardless-of-the-ready-timeout-value.md index bccc30e0db8..00f7e50bdde 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/sdk-never-gets-ready-regardless-of-the-ready-timeout-value.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/sdk-never-gets-ready-regardless-of-the-ready-timeout-value.md @@ -18,7 +18,7 @@ Split SDK never gets ready, regardless of how much the ready timeout value. There are several possible root causes for this issue: -* If the SDK used is a server side type (Python, Ruby, GO, PHP, NodeJS or Java), and the API key used is Client-side type. The Split cloud service is expecting a specific call for Segment information which is different for Client-side vs Server-side API keys. +* If the SDK used is a server side type (Python, Ruby, GO, PHP, Node.js or Java), and the API key used is Client-side type. The Split cloud service is expecting a specific call for Segment information which is different for Client-side vs Server-side API keys. * Verify if there are large Segments in Split environment. Segments that contain tens of thousands of records will require a long time to be downloaded to the SDK cache. * Verify network connection to sdk.split.io is fast. Use the command below to verify: ``` diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-dependency-on-old-version-of-package-url-parse.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-dependency-on-old-version-of-package-url-parse.md index cc3acbc0cbf..2491f04cea6 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-dependency-on-old-version-of-package-url-parse.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-dependency-on-old-version-of-package-url-parse.md @@ -1,6 +1,6 @@ --- -title: "NodeJS SDK: Dependency on old version of package url-parse" -sidebar_label: "NodeJS SDK: Dependency on old version of package url-parse" +title: "Node.js SDK: Dependency on old version of package url-parse" +sidebar_label: "Node.js SDK: Dependency on old version of package url-parse" helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 1 diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-how-to-deploy-in-aws-lambda.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-how-to-deploy-in-aws-lambda.md index 9f0b16d9d0a..93b99d02a6f 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-how-to-deploy-in-aws-lambda.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-how-to-deploy-in-aws-lambda.md @@ -1,6 +1,6 @@ --- -title: How to deploy NodeJS SDK in AWS Lambda -sidebar_label: How to deploy NodeJS SDK in AWS Lambda +title: How to deploy Node.js SDK in AWS Lambda +sidebar_label: How to deploy Node.js SDK in AWS Lambda helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 7 @@ -12,7 +12,7 @@ sidebar_position: 7 ## Question -How to deploy NodeJS SDK code in AWS Lambda service? +How to deploy Node.js SDK code in AWS Lambda service? ## Answer @@ -21,7 +21,7 @@ Prerequisites: 1. We will use similar code to the Javascript SDK example code in this [KB Link](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/Javascript-SDK). Go ahead and download the example and make sure it runs successfully. 2. AWS Lambda supports Node 10.x as of writing this article, make sure to use that version. -Follow these steps to run NodeJS SDK as a Lambda Function: +Follow these steps to run Node.js SDK as a Lambda Function: 1. Create a node project and add the `index.js` file with the content below, make sure to replace the SDK API KEY, USER ID, and SPLIT NAME with corresponding value from your environment. diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-localhost-mode-error-cannot-find-name-path.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-localhost-mode-error-cannot-find-name-path.md index ddb0ec59d51..1f5d28bc9c0 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-localhost-mode-error-cannot-find-name-path.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-localhost-mode-error-cannot-find-name-path.md @@ -1,6 +1,6 @@ --- -title: "NodeJS SDK: While using Localhost mode, error generated: Cannot find name 'path'" -sidebar_label: "NodeJS SDK: While using Localhost mode, error generated: Cannot find name 'path'" +title: "Node.js SDK: While using Localhost mode, error generated: Cannot find name 'path'" +sidebar_label: "Node.js SDK: While using Localhost mode, error generated: Cannot find name 'path'" helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 4 diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-using-gettreatment-in-localhost-mode-does-not-work-with-then-and-catch-blocks.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-using-gettreatment-in-localhost-mode-does-not-work-with-then-and-catch-blocks.md index 897cc522725..b89f58fb94c 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-using-gettreatment-in-localhost-mode-does-not-work-with-then-and-catch-blocks.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-using-gettreatment-in-localhost-mode-does-not-work-with-then-and-catch-blocks.md @@ -1,6 +1,6 @@ --- -title: "NodeJS SDK: Using getTreatment() in localhost mode, does not work with then() and catch() blocks" -sidebar_label: "NodeJS SDK: Using getTreatment() in localhost mode, does not work with then() and catch() blocks" +title: "Node.js SDK: Using getTreatment() in localhost mode, does not work with then() and catch() blocks" +sidebar_label: "Node.js SDK: Using getTreatment() in localhost mode, does not work with then() and catch() blocks" helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 3 @@ -12,7 +12,7 @@ sidebar_position: 3 ## Issue -When implementing NodeJS SDK with Redis storage, the getTreatment method is a wrapper for redis fetch call which returns a promise, which works fine with then() and catch() blocks. +When implementing Node.js SDK with Redis storage, the getTreatment method is a wrapper for redis fetch call which returns a promise, which works fine with then() and catch() blocks. However, testing the SDK code in localhost mode below: diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/nodejs-vue-nuxt-ssr.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/nodejs-vue-nuxt-ssr.md index 8999fe7d3c4..3dce8d9b322 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/nodejs-vue-nuxt-ssr.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/nodejs-vue-nuxt-ssr.md @@ -1,6 +1,6 @@ --- -title: NodeJS Example using Vue Framework and Nuxt library with Server Side Rendering -sidebar_label: NodeJS Example using Vue Framework and Nuxt library with Server Side Rendering +title: Node.js Example using Vue Framework and Nuxt library with Server Side Rendering +sidebar_label: Node.js Example using Vue Framework and Nuxt library with Server Side Rendering helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 2 @@ -10,4 +10,4 @@ sidebar_position: 2

-[NodeJS example using Vue Framework and Nuxt Library](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/NodeJS-withVue-Nuxt) \ No newline at end of file +[Node.js example using Vue Framework and Nuxt Library](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/NodeJS-withVue-Nuxt) \ No newline at end of file From 0c39974119d8336e300853320fbd6ea35c113bb6 Mon Sep 17 00:00:00 2001 From: lena sano Date: Sun, 9 Mar 2025 05:36:43 -0300 Subject: [PATCH 09/19] consistent capitalization --- .../moving-feature-flags-to-a-service.md | 14 +++++------ .../best-practices/split-sync-runbook.md | 2 +- .../client-side-sdk-examples/android-app.md | 6 ++--- .../android-kotlin.md | 4 ++-- .../client-side-sdk-examples/ios-app.md | 6 ++--- .../client-side-sdk-examples/ios-obj-c.md | 6 ++--- .../ios-swift-app-two-factories.md | 6 ++--- .../javascript-code.md | 8 +++---- .../javascript-sdk-nextjs.md | 8 +++---- .../client-side-sdk-examples/javascript.md | 4 ++-- ...s-with-react-redux-using-javascript-sdk.md | 8 +++---- .../react-native-android-app.md | 4 ++-- .../react-native-app-nodejs.md | 6 ++--- .../react-native-ios-app.md | 4 ++-- .../redux-sdk-running-on-client-side.md | 6 ++--- .../client-side-sdks/redux-sdk.md | 2 +- ...ios-javascript-sdk-client-on-never-runs.md | 8 +++---- ...in-sdk-always-returns-control-treatment.md | 4 ++-- .../browser-sdk-migration-guide.md | 24 +++++++++---------- ...how-to-initialize-for-multiple-user-ids.md | 2 +- ...d-browser-sdk-does-the-sdk-cache-expire.md | 2 +- ...changes-roll-out-slowly-to-user-devices.md | 6 ++--- ...-jfbcrypt-m-left-shift-of-x-by-y-places.md | 2 +- ...call-when-running-sdk-in-service-worker.md | 8 +++---- ...sdk-does-sdk-ready-event-fire-only-once.md | 6 ++--- ...-not-supported-by-the-storage-mechanism.md | 8 +++---- ...ploy-javascript-sdk-to-a-wordpress-site.md | 14 +++++------ ...dk-how-to-enable-conent-security-policy.md | 12 +++++----- ...st-mode-does-not-support-allowlist-keys.md | 6 ++--- .../javascript-sdk-mysegments-endpoint.md | 8 +++---- ...t-sdk-not-ready-status-in-slow-networks.md | 8 +++---- ...javascript-sdk-polimer-cli-enoent-error.md | 6 ++--- .../javascript-sdk-react-native.md | 8 +++---- ...o-get-treatments-outside-the-components.md | 8 +++---- ...sdk-lazy-initialization-of-split-client.md | 2 +- .../always-getting-control-treatments.md | 2 +- .../how-to-use-split-sdks-with-split-proxy.md | 2 +- ...ment-function-without-passing-a-user-id.md | 12 +++++----- .../isomorphic-javascript-wrapper-example.md | 8 +++---- ...n-running-in-kubernetes-and-istio-proxy.md | 4 ++-- .../sync-compatibility-matrix.md | 4 ++-- .../java-sdk-how-to-deploy-in-aws-lambda.md | 2 +- .../nodejs-sdk-how-to-deploy-in-aws-lambda.md | 2 +- .../sdk-overview/sdk-validation-checklist.md | 2 +- .../server-side-sdk-examples/go-app.md | 6 ++--- .../go-sdk-localhost-mode-yaml.md | 6 ++--- .../server-side-sdk-examples/java-app.md | 6 ++--- .../java-sdk-scala-sbt-cl.md | 6 ++--- .../net-core-csharp-app.md | 6 ++--- .../server-side-sdk-examples/net-csharp.md | 4 ++-- .../nodejs-vue-nuxt-ssr.md | 4 ++-- .../server-side-sdk-examples/php-app.md | 6 ++--- .../server-side-sdk-examples/python-app.md | 6 ++--- .../server-side-sdk-examples/ruby-app.md | 6 ++--- .../ruby-on-rails-puma.md | 6 ++--- 55 files changed, 168 insertions(+), 168 deletions(-) diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/moving-feature-flags-to-a-service.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/moving-feature-flags-to-a-service.md index ee8d4227b6a..70e5091ce9f 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/moving-feature-flags-to-a-service.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/moving-feature-flags-to-a-service.md @@ -1,6 +1,6 @@ --- -title: Moving Feature Flags to a Service -sidebar_label: Moving Feature Flags to a Service +title: Moving feature flags to a service +sidebar_label: Moving feature flags to a service helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 2 @@ -11,9 +11,9 @@ sidebar_position: 2

-## Using a Service for Feature flags +## Using a service for feature flags -Split lets you roll out features and experiment with a target group of customers across the full web stack: from deep in the backend to client-facing Javascript and mobile. +Split lets you roll out features and experiment with a target group of customers across the full web stack: from deep in the backend to client-facing JavaScript and mobile. Feature flagging in mobile can be particularly advantageous. For example, consider what happens when a critical bug appears in a newly-released mobile feature: due to App Store approval delay, a fix can’t be delivered to customers in minutes; not to mention, you can't force customers to update their apps. @@ -39,7 +39,7 @@ This approach has a number of advantages: * **No impact to file size** By hosting the library on the server side, you need never worry about increasing the footprint of your mobile or IoT app by adding Split’s library. Phoning home is safe. -## Best Practices for Designing the Service +## Best practices for designing the service Split provides the [Evaluator](https://help.split.io/hc/en-us/articles/360020037072-Split-Evaluator) as an out of the box solution for evaluating feature flags on the server-side, to both address potential client-side challenges and to split on applications written in languages for which there is no SDK. @@ -62,7 +62,7 @@ Example: Since dimension values are encoded in the query parameters, we recommend communicating over https. -### Response Schema and Status Code +### Response schema and status code The response object should follow this schema: @@ -105,7 +105,7 @@ if ("on".equals(treatment)) { } ``` -### Server Code Sample +### Server code sample Here is a Guice enabled Java pseudo-code for the REST server: diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/split-sync-runbook.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/split-sync-runbook.md index 5f9ba8bfe00..f086c2c60da 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/split-sync-runbook.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/best-practices/split-sync-runbook.md @@ -98,7 +98,7 @@ We recommend the following alerts: Alerting on CONTROL treatment can also be set at the Split Synchronizer level by setting an impression listener described in the [Split Synchronizer guide.](https://help.split.io/hc/en-us/articles/360019686092-Split-synchronizer-proxy#listener) This approach is similar to the SDK as described at the top of this runbook, but from the Synchronizer standpoint. -### Health Check Monitors +### Health check monitors We have two monitors to periodically validate the Synchronizer health. diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/android-app.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/android-app.md index 34c57edefc4..57d3a6044e4 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/android-app.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/android-app.md @@ -1,6 +1,6 @@ --- -title: Android App Project using Split SDK example -sidebar_label: Android App Project using Split SDK example +title: Android app project using Split SDK example +sidebar_label: Android app project using Split SDK example helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 13 @@ -10,4 +10,4 @@ sidebar_position: 13

-[Android App Project using Split SDK Example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/android-sdk) \ No newline at end of file +[Android app project using Split SDK Example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/android-sdk) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/android-kotlin.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/android-kotlin.md index bd4437ae806..48ee8b40d55 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/android-kotlin.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/android-kotlin.md @@ -1,6 +1,6 @@ --- -title: Android Kotlin App Project using Split SDK example -sidebar_label: Android Kotlin App Project using Split SDK example +title: Android Kotlin app project using Split SDK example +sidebar_label: Android Kotlin app project using Split SDK example helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 7 diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-app.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-app.md index d5f0195d529..3f2635d2039 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-app.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-app.md @@ -1,6 +1,6 @@ --- -title: iOS App Project using Split SDK example -sidebar_label: iOS App Project using Split SDK example +title: iOS app project using Split SDK example +sidebar_label: iOS app project using Split SDK example helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 14 @@ -10,4 +10,4 @@ sidebar_position: 14

-[iOS App Project using Split SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/iOS-Swift-SDK) \ No newline at end of file +[iOS app project using Split SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/iOS-Swift-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-obj-c.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-obj-c.md index 3d55ab3e097..a65357f29e8 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-obj-c.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-obj-c.md @@ -1,6 +1,6 @@ --- -title: iOS Objective-C Project using Split SDK example -sidebar_label: iOS Objective-C Project using Split SDK example +title: iOS Objective-C project using Split SDK example +sidebar_label: iOS Objective-C project using Split SDK example helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 3 @@ -10,4 +10,4 @@ sidebar_position: 3

-[iOS Objective-C Project using Split SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/iOS-Objective-C-SDK) \ No newline at end of file +[iOS Objective-C project using Split SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/iOS-Objective-C-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-swift-app-two-factories.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-swift-app-two-factories.md index ee61370b1e9..b2a83f90d8b 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-swift-app-two-factories.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-swift-app-two-factories.md @@ -1,6 +1,6 @@ --- -title: iOS Swift App Project using Two Split SDK Factories example -sidebar_label: iOS Swift App Project using Two Split SDK Factories example +title: iOS Swift app project using two Split SDK Factories example +sidebar_label: iOS Swift app project using two Split SDK Factories example helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 10 @@ -10,4 +10,4 @@ sidebar_position: 10

-[iOS Swift App Project using Two Split SDK factories example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/iOS-two-factories-SDK) \ No newline at end of file +[iOS Swift app project using Two Split SDK factories example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/iOS-two-factories-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/javascript-code.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/javascript-code.md index b9370f75223..5ef82420f0d 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/javascript-code.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/javascript-code.md @@ -1,13 +1,13 @@ --- -title: Javascript Code using Split SDK example -sidebar_label: Javascript Code using Split SDK example +title: JavaScript code using Split SDK example +sidebar_label: JavaScript code using Split SDK example helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 11 ---

- +

-[Javascript Code using Split SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/Javascript-SDK) \ No newline at end of file +[JavaScript Code using Split SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/JavaScript-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/javascript-sdk-nextjs.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/javascript-sdk-nextjs.md index 4fa62b38e61..57f0420405a 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/javascript-sdk-nextjs.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/javascript-sdk-nextjs.md @@ -1,13 +1,13 @@ --- -title: Javascript SDK Example using Next.js -sidebar_label: Javascript SDK Example using Next.js +title: JavaScript SDK example using Next.js +sidebar_label: JavaScript SDK example using Next.js helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 6 ---

- +

-[Javascript SDK Example using Next.JS](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/JavasScript-with-NextJS) \ No newline at end of file +[JavaScript SDK example using Next.js](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/JavasScript-with-NextJS) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/javascript.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/javascript.md index 81b9764a9e8..59416a70dde 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/javascript.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/javascript.md @@ -1,6 +1,6 @@ --- -title: JavaScript SDK used with JavaScript Frameworks -sidebar_label: JavaScript SDK used with JavaScript Frameworks +title: JavaScript SDK used with JavaScript frameworks +sidebar_label: JavaScript SDK used with JavaScript frameworks helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 15 diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/nodejs-with-react-redux-using-javascript-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/nodejs-with-react-redux-using-javascript-sdk.md index c91c2b6a511..f65f326bc6d 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/nodejs-with-react-redux-using-javascript-sdk.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/nodejs-with-react-redux-using-javascript-sdk.md @@ -1,16 +1,16 @@ --- -title: Node.js with React Redux Project using Split Javascript SDK example -sidebar_label: Node.js with React Redux Project using Split Javascript SDK example +title: Node.js with React Redux project using Split JavaScript SDK example +sidebar_label: Node.js with React Redux project using Split JavaScript SDK example helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 12 ---

- +

-Example: Basic Code to use Javascript Split SDK 10.3.3 +Example: Basic Code to use JavaScript Split SDK 10.3.3 Environment: diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-android-app.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-android-app.md index 89356bcc349..35e37ba3f5d 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-android-app.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-android-app.md @@ -1,6 +1,6 @@ --- -title: React Native Android App using Split SDK example -sidebar_label: React Native Android App using Split SDK example +title: React Native Android app using Split SDK example +sidebar_label: React Native Android app using Split SDK example helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 9 diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-app-nodejs.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-app-nodejs.md index f66c35d7073..fa311f18e64 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-app-nodejs.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-app-nodejs.md @@ -1,6 +1,6 @@ --- -title: React Native App using Split Node.js SDK example -sidebar_label: React Native App using Split Node.js SDK example +title: React Native app using Split Node.js SDK example +sidebar_label: React Native app using Split Node.js SDK example helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 4 @@ -10,7 +10,7 @@ sidebar_position: 4

-Example: Basic example for React Native App Project using Split Javascript SDK +Example: Basic example for React Native app project using Split JavaScript SDK Example Repo: https://github.com/splitio/react-native-sdk-example diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-ios-app.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-ios-app.md index 3484403b3e4..2a4caa50391 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-ios-app.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-ios-app.md @@ -1,6 +1,6 @@ --- -title: React Native iOS App using Split SDK example -sidebar_label: React Native iOS App using Split SDK example +title: React Native iOS app using Split SDK example +sidebar_label: React Native iOS app using Split SDK example helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 8 diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/redux-sdk-running-on-client-side.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/redux-sdk-running-on-client-side.md index edec4b414ae..2ef6a1ee18b 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/redux-sdk-running-on-client-side.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/redux-sdk-running-on-client-side.md @@ -1,6 +1,6 @@ --- -title: Redux SDK Running on Client Side Example -sidebar_label: Redux SDK Running on Client Side Example +title: Redux SDK running on client side example +sidebar_label: Redux SDK running on client side example helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 5 @@ -10,4 +10,4 @@ sidebar_position: 5

-[Redux SDK Running on client side example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/Redux-Client-side-SDK) \ No newline at end of file +[Redux SDK running on client side example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/Redux-Client-side-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/redux-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/redux-sdk.md index d32088f01b5..d35e2ea2238 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/redux-sdk.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/redux-sdk.md @@ -59,7 +59,7 @@ You need to combine the Split reducer with yours when creating your store and us For the client side, the Redux documentation [recommends](https://redux.js.org/introduction/getting-started#basic-example) creating a single store to be used as the source of truth for your state. This is where we'll plug in the Split reducer. -For Server Side Rendering, the Redux documentation [suggests](https://redux.js.org/usage/server-rendering#handling-the-request) creating a store per request, which is why we provide a function to create stores, where each instance will include the Split reducer. +For Server-Side Rendering, the Redux documentation [suggests](https://redux.js.org/usage/server-rendering#handling-the-request) creating a store per request, which is why we provide a function to create stores, where each instance will include the Split reducer. diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-ios-javascript-sdk-client-on-never-runs.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-ios-javascript-sdk-client-on-never-runs.md index 2d2eded28f9..d34289349b9 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-ios-javascript-sdk-client-on-never-runs.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-ios-javascript-sdk-client-on-never-runs.md @@ -1,6 +1,6 @@ --- -title: "Mobile SDK: When using client.on method, the code block never called" -sidebar_label: "Mobile SDK: When using client.on method, the code block never called" +title: "Mobile SDK: When using client.on method, the code block is never called" +sidebar_label: "Mobile SDK: When using client.on method, the code block is never called" helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 22 @@ -12,7 +12,7 @@ sidebar_position: 22 ## Issue -Using Javascript browser-side, Android or iOS SDKs, and implementing the code below, the code block never gets executed which indicates SDK_READY event never fires. +Using JavaScript browser-side, Android or iOS SDKs, and implementing the code below, the code block never gets executed which indicates SDK_READY event never fires. ```javascript client.on(SplitEvent.SDK_READY, new SplitEventTask() { @@ -85,7 +85,7 @@ class SplitWrapper { ``` - + ```javascript class SplitIO { diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-using-kotlin-sdk-always-returns-control-treatment.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-using-kotlin-sdk-always-returns-control-treatment.md index 9a93a79dca8..8f2293d28bc 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-using-kotlin-sdk-always-returns-control-treatment.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-using-kotlin-sdk-always-returns-control-treatment.md @@ -1,6 +1,6 @@ --- -title: "Android SDK: Using Kotlin, SDK always returns control treatment" -sidebar_label: "Android SDK: Using Kotlin, SDK always returns control treatment" +title: "Android SDK: Using Kotlin, SDK always returns the control treatment" +sidebar_label: "Android SDK: Using Kotlin, SDK always returns the control treatment" helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 17 diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/browser-sdk-migration-guide.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/browser-sdk-migration-guide.md index 2cb82fa78a5..84347445ed5 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/browser-sdk-migration-guide.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/browser-sdk-migration-guide.md @@ -10,17 +10,17 @@ sidebar_position: 6

-Refer to this document to check API differences and migration details for moving from **Javascript SDK v10.15.x** using [NPM](https://www.npmjs.com/package/@splitsoftware/splitio) or [Github](https://github.com/splitio/javascript-client) to **Browser SDK v0.1.x** using [NPM](https://www.npmjs.com/package/@splitsoftware/splitio-browserjs) or [Github](https://github.com/splitio/javascript-browser-client). +Refer to this document to check API differences and migration details for moving from **JavaScript SDK v10.15.x** using [NPM](https://www.npmjs.com/package/@splitsoftware/splitio) or [Github](https://github.com/splitio/javascript-client) to **Browser SDK v0.1.x** using [NPM](https://www.npmjs.com/package/@splitsoftware/splitio-browserjs) or [Github](https://github.com/splitio/javascript-browser-client). ## Requirements -Browser SDK has the [same browser requirements](https://help.split.io/hc/en-us/articles/360058730852-Browser-SDK#language-support) as Javascript SDK (it supports ES5 syntax and requires Promises support) but also requires [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) support. +Browser SDK has the [same browser requirements](https://help.split.io/hc/en-us/articles/360058730852-Browser-SDK#language-support) as JavaScript SDK (it supports ES5 syntax and requires Promises support) but also requires [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) support. Therefore, to target old browsers such as IE10, users must polyfill the Fetch API besides Promises. More details [here](https://help.split.io/hc/en-us/articles/360058730852-Browser-SDK#language-support). ## Installation and import -| | Javascript SDK 10.15.x | Browser SDK 0.1.x | +| | JavaScript SDK 10.15.x | Browser SDK 0.1.x | | --- | --- | --- | | Install NPM package | `> npm install @splitsoftware/splitio` | `> npm install @splitsoftware/splitio-browserjs` | | Import with ES6 module syntax | `import { SplitFactory } from ‘@splitsoftware/splitio’` | `import { SplitFactory } from ‘@splitsoftware/splitio-browserjs’` | @@ -30,11 +30,11 @@ Therefore, to target old browsers such as IE10, users must polyfill the Fetch AP ## Configuration and API -Most configuration params are the same in [Javascript SDK](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#configuration) and [Browser SDK](https://help.split.io/hc/en-us/articles/360058730852-Browser-SDK#configuration). SDK client and manager APIs (i.e., method signatures) are also the same. The differences: +Most configuration params are the same in [JavaScript SDK](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#configuration) and [Browser SDK](https://help.split.io/hc/en-us/articles/360058730852-Browser-SDK#configuration). SDK client and manager APIs (i.e., method signatures) are also the same. The differences: ### Traffic type -| Javascript SDK 10.15.x | Browser SDK 0.1.x | +| JavaScript SDK 10.15.x | Browser SDK 0.1.x | | --- | --- | | Clients can be bound to a traffic type to track events without the need to pass the traffic type.
var factory = SplitFactory(\{

core: \{

authorizationKey: 'YOUR_API_KEY',

key: 'USER_ID',

trafficType: 'YOUR_CUSTOMER_TRAFFIC_TYPE'

\}

\});



// Must not pass traffic type to track call if provided on the factory settings



var mainClient = factory.client();

mainClient.track('EVENT_TYPE', eventValue);



// or when creating a new client with a traffic type.



var newClient = factory.client('NEW_KEY', 'NEW_TRAFFIC_TYPE');

newClient.track('EVENT_TYPE', eventValue);
| Clients cannot be bound to a traffic type, so for tracking events we always need to pass the traffic type. This simplifies the `track` method signature, by removing ambiguity of when it should receive the traffic type or not. |
var factory = SplitFactory(\{

core: \{

authorizationKey: 'YOUR_API_KEY',

key: 'USER_ID',

trafficType: 'YOUR_CUSTOMER_TRAFFIC_TYPE'

\}

\});



// Must not pass traffic type to track call if provided on the factory settings



var mainClient = factory.client();

mainClient.track('EVENT_TYPE', eventValue);



// or when creating a new client with a traffic type.



var newClient = factory.client('NEW_KEY', 'NEW_TRAFFIC_TYPE');

newClient.track('EVENT_TYPE', eventValue);
| NOT ALLOWED
The `core.trafficType` config param, and the second param of `factory.client()` are ignored. | @@ -91,17 +91,17 @@ const { In the Browser SDK, you must “plug” a logger instance in the `debug` config param to have human-readable message codes. -You can set a boolean or string log level value as `debug` config param, as in the regular Javascript SDK, but in that case, most log messages will display a code number instead. +You can set a boolean or string log level value as `debug` config param, as in the regular JavaScript SDK, but in that case, most log messages will display a code number instead. Those message codes are listed in the public repository: [`Error`](https://github.com/splitio/javascript-commons/blob/development/src/logger/messages/error.ts), [`Warning`](https://github.com/splitio/javascript-commons/blob/development/src/logger/messages/warn.ts), [`Info`](https://github.com/splitio/javascript-commons/blob/development/src/logger/messages/info.ts), [`Debug`](https://github.com/splitio/javascript-commons/blob/development/src/logger/messages/debug.ts). More details [here](https://help.split.io/hc/en-us/articles/360058730852-Browser-SDK#logging). -| Javascript SDK 10.15.x | Browser SDK 0.1.x | +| JavaScript SDK 10.15.x | Browser SDK 0.1.x | | --- | --- | |
import \{ SplitFactory \} from '@splitsoftware/splitio'



var factory = SplitFactory(\{

…,

debug: 'DEBUG' // other options are: true, false,

// 'INFO', 'WARN' and 'ERROR'

\});

|
import \{ SplitFactory, DebugLogger \} from '@splitsoftware/splitio-browserjs'



var factory = SplitFactory(\{

…,

debug: DebugLogger() // other options are: true, false, 'DEBUG',

// 'INFO', 'WARN', 'ERROR', InfoLogger(),

// WarnLogger(), and ErrorLogger()

\});
| ### Configuring LocalStorage -| Javascript SDK 10.15.x | Browser SDK 0.1.x | +| JavaScript SDK 10.15.x | Browser SDK 0.1.x | | --- | --- | |
import \{ SplitFactory \} from '@splitsoftware/splitio'



var sdk = SplitFactory(\{

core: \{

authorizationKey: 'YOUR_API_KEY',

key: 'USER_ID',

\},



storage: \{

type: 'LOCALSTORAGE',

prefix: 'MYPREFIX'

\}

});
|
import \{

SplitFactory,

InLocalStorage

\} from '@splitsoftware/splitio-browserjs'



var sdk = SplitFactory(\{

core: \{

authorizationKey: 'YOUR_API_KEY',

key: 'USER_ID',

},



storage: InLocalStorage(\{

prefix: 'MYPREFIX'

\})

\});
| @@ -109,14 +109,14 @@ Those message codes are listed in the public repository: [`Error`](https://githu ### Configuring integrations -| Javascript SDK 10.15.x | Browser SDK 0.1.x | +| JavaScript SDK 10.15.x | Browser SDK 0.1.x | | --- | --- | |
import \{ SplitFactory \} from '@splitsoftware/splitio'



var factory = SplitFactory(\{

core: \{

authorizationKey: 'YOUR_API_KEY',

key: 'USER_ID',

\},



integrations: [

\{

type: 'GOOGLE_ANALYTICS_TO_SPLIT',

identities: [\{

key: 'USER_ID',

trafficType: 'user'

\}]



\}, \{

type: 'SPLIT_TO_GOOGLE_ANALYTICS'

\}

]

\});
|
import \{

SplitFactory,

GoogleAnalyticsToSplit,

SplitToGoogleAnalytics

\} from '@splitsoftware/splitio-browserjs'



var factory = SplitFactory(\{

core: \{

authorizationKey: 'YOUR_API_KEY',

key: 'USER_ID'

\},

integrations: [

GoogleAnalyticsToSplit(\{

identities: [\{

key: 'USER_ID',

trafficType: 'user'

\}]

\}),



SplitToGoogleAnalytics()

]

\});
| ### Localhost mode Not supported. Split hasn’t yet decided how to expose this feature as a pluggable module in favor of size reduction. -| Javascript SDK 10.15.x | Browser SDK 0.1.x | +| JavaScript SDK 10.15.x | Browser SDK 0.1.x | | --- | --- | |
var sdk = SplitFactory(\{

core: \{

authorizationKey: 'localhost'

\},



features: \{

'reporting_v2': 'on', // example with just a string value for the treatment

'billing_updates': \{ treatment: 'visa', config: '\{ "color": "blue" \}' \} //example of a defined config

'show_status_bar': \{ treatment: 'off', config: null \} // example of a null config

\},

scheduler: \{

offlineRefreshRate: 15 // 15 sec

\}

\});
| NOT SUPPORTED YET | --> @@ -125,12 +125,12 @@ Not supported. Split hasn’t yet decided how to expose this feature as a plugg CDN bundle size: -* Javascript SDK (https://cdn.split.io/sdk/split-10.15.4.min.js ): 126656 B (123.7 kB) +* JavaScript SDK (https://cdn.split.io/sdk/split-10.15.4.min.js ): 126656 B (123.7 kB) * Browser SDK * Slim/regular (https://cdn.split.io/sdk/split-browser-0.1.0.min.js ): 69338 B (67.7 kB) * Full (https://cdn.split.io/sdk/split-browser-0.1.0.full.min.js ): 93163 B (91.0 kB) NPM package size: -* [Javascript SDK](https://bundlephobia.com/result?p=@splitsoftware/splitio@10.15.4): 109.7 kB minified +* [JavaScript SDK](https://bundlephobia.com/result?p=@splitsoftware/splitio@10.15.4): 109.7 kB minified * [Browser SDK](https://bundlephobia.com/result?p=@splitsoftware/splitio-browserjs@0.1.0): 87.2 kB minified \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-and-android-sdk-how-to-initialize-for-multiple-user-ids.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-and-android-sdk-how-to-initialize-for-multiple-user-ids.md index 060f0c01e97..9faf4abf282 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-and-android-sdk-how-to-initialize-for-multiple-user-ids.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-and-android-sdk-how-to-initialize-for-multiple-user-ids.md @@ -12,7 +12,7 @@ sidebar_position: 19 ## Question -The Javascript SDK is capable of initializing multiple client objects from the same Split factory object, each with their unique user key (user id): +The JavaScript SDK is capable of initializing multiple client objects from the same Split factory object, each with their unique user key (user id): ```javascript client1 = factory.client("user_id1"); diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-android-browser-sdk-does-the-sdk-cache-expire.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-android-browser-sdk-does-the-sdk-cache-expire.md index dcd78db1580..5818f4115e2 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-android-browser-sdk-does-the-sdk-cache-expire.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-android-browser-sdk-does-the-sdk-cache-expire.md @@ -12,7 +12,7 @@ sidebar_position: 8 ## Question -The Split mobile (iOS and Android) and Javascript browser SDKs download a local cache and store it in a file system. Does the cache have an expire date or TTL? +The Split mobile (iOS and Android) and JavaScript browser SDKs download a local cache and store it in a file system. Does the cache have an expire date or TTL? ## Answer diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-android-browser-sdk-split-changes-roll-out-slowly-to-user-devices.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-android-browser-sdk-split-changes-roll-out-slowly-to-user-devices.md index ff8a88679d1..f66e2fead47 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-android-browser-sdk-split-changes-roll-out-slowly-to-user-devices.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-android-browser-sdk-split-changes-roll-out-slowly-to-user-devices.md @@ -1,6 +1,6 @@ --- -title: "Mobile and web SDK: Split changes roll out slowly to user devices." -sidebar_label: "Mobile and web SDK: Split changes roll out slowly to user devices." +title: "Mobile and web SDK: Split changes roll out slowly to user devices" +sidebar_label: "Mobile and web SDK: Split changes roll out slowly to user devices" helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 7 @@ -12,7 +12,7 @@ sidebar_position: 7 ## Issue -When making a change to a feature flag through the web UI, mobile (iOS and Android) and Javascript Browser SDKs do not reflect that change at the same time. A small population of devices are synched in the first day, then more user devices get synched in subsequent days until all SDKs are updated. +When making a change to a feature flag through the web UI, mobile (iOS and Android) and JavaScript Browser SDKs do not reflect that change at the same time. A small population of devices are synched in the first day, then more user devices get synched in subsequent days until all SDKs are updated. Why do Split changes propagate slowly to user devices? diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-sdk-runtime-error-jfbcrypt-m-left-shift-of-x-by-y-places.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-sdk-runtime-error-jfbcrypt-m-left-shift-of-x-by-y-places.md index 4587dc1c18c..2a4bc6ccb5c 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-sdk-runtime-error-jfbcrypt-m-left-shift-of-x-by-y-places.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-sdk-runtime-error-jfbcrypt-m-left-shift-of-x-by-y-places.md @@ -12,7 +12,7 @@ sidebar_position: 13 ## Issue -Using Objective-C Project with iOS SDK, the following runtime error shows as soon as the Split factory object is initialized: +Using Objective-C project with iOS SDK, the following runtime error shows as soon as the Split factory object is initialized: ``` .../Pods/Split/Split/Common/Utils/JFBCrypt/JFBCrypt.m:578:16: runtime error: left shift of 16488694 by 8 places cannot be represented in type 'SInt32' (aka 'int') diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-cors-error-in-streaming-call-when-running-sdk-in-service-worker.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-cors-error-in-streaming-call-when-running-sdk-in-service-worker.md index daaa3a4a98a..f02e14a3f72 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-cors-error-in-streaming-call-when-running-sdk-in-service-worker.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-cors-error-in-streaming-call-when-running-sdk-in-service-worker.md @@ -1,18 +1,18 @@ --- -title: "Javascript SDK: CORS Error in streaming call when running SDK in Service Worker" -sidebar_label: "Javascript SDK: CORS Error in streaming call when running SDK in Service Worker" +title: "JavaScript SDK: CORS Error in streaming call when running SDK in Service Worker" +sidebar_label: "JavaScript SDK: CORS Error in streaming call when running SDK in Service Worker" helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 4 ---

- +

## Issue -When running the Javascript SDK inside Service Worker, the SDK Streaming http call to streaming.split.io is blocked by CORS browser policy as shown below: +When running the JavaScript SDK inside Service Worker, the SDK Streaming http call to streaming.split.io is blocked by CORS browser policy as shown below: ![](https://help.split.io/hc/article_attachments/4415274038285) diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-does-sdk-ready-event-fire-only-once.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-does-sdk-ready-event-fire-only-once.md index 7bca6a3db41..a2a136449c8 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-does-sdk-ready-event-fire-only-once.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-does-sdk-ready-event-fire-only-once.md @@ -1,13 +1,13 @@ --- -title: "Javascript SDK: Does SDK_READY event fire only once?" -sidebar_label: "Javascript SDK: Does SDK_READY event fire only once?" +title: "JavaScript SDK: Does SDK_READY event fire only once?" +sidebar_label: "JavaScript SDK: Does SDK_READY event fire only once?" helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 9 ---

- +

## Problem diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-error-shared-client-not-supported-by-the-storage-mechanism.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-error-shared-client-not-supported-by-the-storage-mechanism.md index f6687598402..7a61d47d943 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-error-shared-client-not-supported-by-the-storage-mechanism.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-error-shared-client-not-supported-by-the-storage-mechanism.md @@ -1,18 +1,18 @@ --- -title: "Javascript SDK Error: \"Shared Client not supported by the storage mechanism. Create isolated instances instead\"" -sidebar_label: "Javascript SDK Error: \"Shared Client not supported by the storage mechanism. Create isolated instances instead\"" +title: "JavaScript SDK Error: \"Shared Client not supported by the storage mechanism. Create isolated instances instead\"" +sidebar_label: "JavaScript SDK Error: \"Shared Client not supported by the storage mechanism. Create isolated instances instead\"" helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 20 ---

- +

## Issue -When testing Javascript SDK browser mode using Jest, it fails with the following error: +When testing JavaScript SDK browser mode using Jest, it fails with the following error: Shared Client not supported by the storage mechanism. Create isolated instances in stead diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-how-to-deploy-javascript-sdk-to-a-wordpress-site.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-how-to-deploy-javascript-sdk-to-a-wordpress-site.md index 75d2ae92235..19186f675e3 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-how-to-deploy-javascript-sdk-to-a-wordpress-site.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-how-to-deploy-javascript-sdk-to-a-wordpress-site.md @@ -1,24 +1,24 @@ --- -title: How to deploy Javascript SDK to a Wordpress site? -sidebar_label: How to deploy Javascript SDK to a Wordpress site? +title: How to deploy JavaScript SDK to a Wordpress site? +sidebar_label: How to deploy JavaScript SDK to a Wordpress site? helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 14 ---

- +

## Question -How to deploy Javascript SDK code in a Wordpress site? +How to deploy JavaScript SDK code in a Wordpress site? ## Answer -The steps below explain how to use Javascript SDK in a blank page within a Wordpress site +The steps below explain how to use JavaScript SDK in a blank page within a Wordpress site -1. First step is to install Header and Footer Scripts plugin. While this is not required, it is a good practice to load the Javascript SDK library within the page header. +1. First step is to install Header and Footer Scripts plugin. While this is not required, it is a good practice to load the JavaScript SDK library within the page header. ![](https://help.split.io/hc/article_attachments/360060037831/Screen_Shot_2020-06-18_at_11.25.35_AM.png) @@ -38,7 +38,7 @@ The steps below explain how to use Javascript SDK in a blank page within a Wordp ![](https://help.split.io/hc/article_attachments/360059871812/Screen_Shot_2020-06-18_at_11.54.52_AM.png) -5. The Custom HTML block allows any HTML elements, including Javascript, copy and paste the code below inside it, make sure to replace the API KEY with a valid key, set the User key and feature flag name as well. +5. The Custom HTML block allows any HTML elements, including JavaScript, copy and paste the code below inside it, make sure to replace the API KEY with a valid key, set the User key and feature flag name as well. ```javascript

diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-how-to-enable-conent-security-policy.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-how-to-enable-conent-security-policy.md index bbb00062f95..ba3541625ac 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-how-to-enable-conent-security-policy.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-how-to-enable-conent-security-policy.md @@ -1,30 +1,30 @@ --- -title: "Javascript SDK: How to enable Content Security Policy (CSP) to work with Javascript SDK" -sidebar_label: "Javascript SDK: How to enable Content Security Policy (CSP) to work with Javascript SDK" +title: "JavaScript SDK: How to enable Content Security Policy (CSP) to work with JavaScript SDK" +sidebar_label: "JavaScript SDK: How to enable Content Security Policy (CSP) to work with JavaScript SDK" helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 21 ---

- +

## Question -Is it possible to enable SCP (Content Security Policy) on a site that uses Split Javascript SDK? +Is it possible to enable SCP (Content Security Policy) on a site that uses Split JavaScript SDK? ## Answer Content Security Policy (CSP) is a computer security standard introduced to prevent cross-site scripting (XSS), clickjacking and other code injection attacks, as defined by this wikipedia article. -It is possible to allow SCP and enable running Split Javascript SDK safely. +It is possible to allow SCP and enable running Split JavaScript SDK safely. There are multiple ways to achieve this, the steps below use "nonce" keyword to target the script block. Make sure the server response header contains the following: Content-Security-Policy: script-src 'self' cdn.split.io 'nonce-swfT4W3546RtDw4'; -On the Javascript side, use this tag for the JS code that uses the SDK: +On the JavaScript side, use this tag for the JS code that uses the SDK: ``` ``` -This script captures regular JavaScript errors and unhandled promise rejections, and stores them in memory. Once the RUM Agent loads, it sends the captured errors to Split services for processing, ensuring that even errors occurring before the Agent is fully loaded are not missed. +This script captures regular JavaScript errors and unhandled promise rejections, and stores them in memory. Once the RUM Agent loads, it sends the captured errors to FME services for processing, ensuring that even errors occurring before the Agent is fully loaded are not missed. ::: ### User consent -By default the Agent will send events to Split cloud, but you can disable this behavior until user consent is explicitly granted. +By default the Agent will send events to Harness FME servers, but you can disable this behavior until user consent is explicitly granted. The `userConsent` configuration parameter lets you set the initial consent status of the Agent, and the `SplitRumAgent.setUserConsent(boolean)` method lets you grant (enable) or decline (disable) dynamic event tracking. There are three possible initial states: - * `'GRANTED'`: The user grants consent for tracking events. The Agent sends them to Split cloud. This is the default value if `userConsent` param is not defined. - * `'DECLINED'`: The user declines consent for tracking events. The Agent does not send them to Split cloud. + * `'GRANTED'`: The user grants consent for tracking events. The Agent sends them to Harness FME servers. This is the default value if `userConsent` param is not defined. + * `'DECLINED'`: The user declines consent for tracking events. The Agent does not send them to Harness FME servers. * `'UNKNOWN'`: The user neither grants nor declines consent for tracking events. The Agent tracks them in its internal storage, and eventually either sends them or not if the consent status is updated to `'GRANTED'` or `'DECLINED'` respectively. The status can be updated at any time with the `setUserConsent` method. @@ -469,7 +469,7 @@ Working with user consent is demonstrated below. SplitRumAgent.setup('YOUR_SDK_KEY', { // Overwrites the initial consent status of the Agent, which is 'GRANTED' by default. // 'UNKNOWN' status represents that the user has neither granted nor declined consent for tracking data, - // so the Agent will locally track data but not send it to Split cloud until consent is changed to 'GRANTED'. + // so the Agent will locally track data but not send it to Harness FME servers until consent is changed to 'GRANTED'. userConsent: 'UNKNOWN' }); @@ -478,10 +478,10 @@ SplitRumAgent.getUserConsent() === 'UNKNOWN'; // `setUserConsent` method lets you update the consent status at any time. // Pass `true` for 'GRANTED' and `false` for 'DECLINED'. -SplitRumAgent.setUserConsent(true); // Consent status changed from 'UNKNOWN' to 'GRANTED'. Data will be sent to Split cloud. +SplitRumAgent.setUserConsent(true); // Consent status changed from 'UNKNOWN' to 'GRANTED'. Data will be sent to Harness FME servers. SplitRumAgent.getUserConsent() === 'GRANTED'; -SplitRumAgent.setUserConsent(false); // Consent status changed from 'GRANTED' to 'DECLINED'. Data will not be sent to Split cloud. +SplitRumAgent.setUserConsent(false); // Consent status changed from 'GRANTED' to 'DECLINED'. Data will not be sent to Harness FME servers. SplitRumAgent.getUserConsent() === 'DECLINED'; ``` @@ -489,6 +489,6 @@ SplitRumAgent.getUserConsent() === 'DECLINED'; ## Example apps -The following repository contains different example apps that demonstrate how to use Split's Browser RUM Agent: +The following repository contains different example apps that demonstrate how to use FME's Browser RUM Agent: * [Browser RUM Agent examples](https://github.com/splitio/browser-rum-agent-examples) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-agents/ios-rum-agent.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-agents/ios-rum-agent.md index 2ccc5e01525..0eb8b8f3aad 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-agents/ios-rum-agent.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-agents/ios-rum-agent.md @@ -12,17 +12,17 @@ helpdocs_is_published: true import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -This guide provides detailed information about Split's Real User Monitoring (RUM) Agent for iOS. +This guide provides detailed information about FME's Real User Monitoring (RUM) Agent for iOS. -Split's iOS RUM Agent collects events about your users' experience when they use your application and sends this information to Split services. This allows you to measure and analyze the impact of feature flag changes on performance metrics. +FME's iOS RUM Agent collects events about your users' experience when they use your application and sends this information to FME services. This allows you to measure and analyze the impact of feature flag changes on performance metrics. ## Language Support -Split's iOS RUM Agent is designed for iOS applications written in Swift and is compatible with iOS SDK versions 12 and later. +FME's iOS RUM Agent is designed for iOS applications written in Swift and is compatible with iOS SDK versions 12 and later. ## Initialization -Set up Split's RUM Agent in your code with the following three steps: +Set up FME's RUM Agent in your code with the following three steps: ### 1. Import the Agent into your project @@ -42,14 +42,14 @@ If you get the `Sandbox: rsync.samba(19690) deny(1) file-read-data ...` error, m ### 2. Setup the Agent -To allow the Agent to send information to Split services, you need to call the `setup` method on the `SplitRum` object. +To allow the Agent to send information to FME services, you need to call the `setup` method on the `SplitRum` object. ```swift title="Swift" try? SplitRum.setup(apiKey: "YOUR_SDK_KEY") ``` :::warning[Important] -The Crashlytics framework has a compatibility issue that interferes with the proper functioning of Split RUM Agent when Crashlytics is initialized first. To resolve this, initialize the Split RUM Agent using the setup method before starting Crashlytics. +The Crashlytics framework has a compatibility issue that interferes with the proper functioning of RUM Agent when Crashlytics is initialized first. To resolve this, initialize the RUM Agent using the setup method before starting Crashlytics. ```swift try? SplitRum.setup(apiKey: "YOUR_SDK_KEY") @@ -78,9 +78,9 @@ Arguments passed to the `setup` method will override any value contained in the ### 3. Add an Identity -While the Agent will work without having an Identity, events won't be sent to Split services until at least one is set. +While the Agent will work without having an Identity, events won't be sent to FME services until at least one is set. -Identity objects consist of a key and a [traffic type](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type). You can only pass values that match the names of traffic types already defined in the Split Management Console. +Identity objects consist of a key and a [traffic type](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type). You can only pass values that match the names of traffic types already defined in Harness FME. The RUM Agent provides methods to manage Identities, as shown in the table below. @@ -103,7 +103,7 @@ SplitRum.removeIdentities() ## Configuration -Split's iOS RUM Agent can be configured to change its default behavior. The following options are available: +FME's iOS RUM Agent can be configured to change its default behavior. The following options are available: - Log Level: level of logging. Valid values are `VERBOSE`, `DEBUG`, `INFO`, `WARNING`, `ERROR` and `NONE`. Log level can be configured using the `SplitRumAgent-Info.plist` file or programmatically. Values specified programmatically will override any of the same values specified in the configuration file. @@ -132,7 +132,7 @@ SplitRum.setup(apiKey: "YOUR_SDK_KEY", config: config) ## Events -Split's iOS RUM Agent collects a number of events by default. +FME's iOS RUM Agent collects a number of events by default. ### Default events and properties @@ -152,7 +152,7 @@ Each event for the metrics described above automatically includes a `session_id` ## Automatic metric creation -Split will automatically create [metrics](https://help.split.io/hc/en-us/articles/22005565241101-Metrics) for a subset of the event types received from the iOS RUM Agent. These "out of the box metrics" are auto-created for you: +FME will automatically create [metrics](https://help.split.io/hc/en-us/articles/22005565241101-Metrics) for a subset of the event types received from the iOS RUM Agent. These "out of the box metrics" are auto-created for you: | **Event type** | **Metric name** | | --- | --- | @@ -161,7 +161,7 @@ Split will automatically create [metrics](https://help.split.io/hc/en-us/article | split.rum.app_start | Average App Start Time - Split Agents | | split.rum.anr | Count of ANRs - Split Agents | -For a metric that was auto-created, you can manage the [definition](https://help.split.io/hc/en-us/articles/22005565241101-Metrics) and [alert policies](https://help.split.io/hc/en-us/articles/19832312225293-Configuring-metric-alerting) like you would for any other metric. If you delete a metric that was auto-created, Split will not re-create the metric, even if the event type is still flowing. +For a metric that was auto-created, you can manage the [definition](https://help.split.io/hc/en-us/articles/22005565241101-Metrics) and [alert policies](https://help.split.io/hc/en-us/articles/19832312225293-Configuring-metric-alerting) like you would for any other metric. If you delete a metric that was auto-created, FME will not re-create the metric, even if the event type is still flowing. ## Advanced use cases @@ -219,13 +219,13 @@ SplitRum.trackTimeFromStart(marker: "data_loaded") ### User consent -By default the Agent will send events to Split cloud, but you can disable this behavior until user consent is explicitly granted. +By default the Agent will send events to Harness FME servers, but you can disable this behavior until user consent is explicitly granted. The `userConsent` configuration parameter lets you set the initial consent status of the Agent, and the `SplitRum.setUserConsent(boolean)` method lets you grant (enable) or decline (disable) dynamic event tracking. There are three possible initial states: - * `'GRANTED'`: The user grants consent for tracking events and impressions. The SDK sends them to Split cloud. This is the default value if `userConsent` param is not defined. - * `'DECLINED'`: The user declines consent for tracking events and impressions. The SDK does not send them to Split cloud. + * `'GRANTED'`: The user grants consent for tracking events and impressions. The SDK sends them to Harness FME servers. This is the default value if `userConsent` param is not defined. + * `'DECLINED'`: The user declines consent for tracking events and impressions. The SDK does not send them to Harness FME servers. * `'UNKNOWN'`: The user neither grants nor declines consent for tracking events and impressions. The SDK tracks them in its internal storage, and eventually either sends them or not if the consent status is updated to `'GRANTED'` or `'DECLINED'` respectively. The status can be updated at any time with the `setUserConsent` factory method. @@ -237,12 +237,12 @@ Working with user consent is demonstrated below. // 'UNKNOWN' status represents that the user has neither granted nor declined consent for tracking data, let cfg = SplitRumConfig().userConsent(.unknown) - // so the Agent locally tracks data but not send it to Split cloud until consent is changed to 'GRANTED'. + // so the Agent locally tracks data but not send it to Harness FME servers until consent is changed to 'GRANTED'. try? SplitRum.setup(apiKey: apiKey, config: config) - // Changed User Consent status to 'GRANTED'. Data will be sent to Split cloud. + // Changed User Consent status to 'GRANTED'. Data will be sent to Harness FME servers. SlitRum.setUserConsent(enabled: true); - // Changed User Consent status to 'DECLINED'. Data will not be sent to Split cloud. + // Changed User Consent status to 'DECLINED'. Data will not be sent to Harness FME servers. SlitRum.setUserConsent(enabled: false); // The 'getUserConsent' method returns User Consent status. diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/android-app.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/android-app.md index 57d3a6044e4..3349704c847 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/android-app.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/android-app.md @@ -1,6 +1,6 @@ --- -title: Android app project using Split SDK example -sidebar_label: Android app project using Split SDK example +title: Android app project using FME SDK example +sidebar_label: Android app project using FME SDK example helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 13 @@ -10,4 +10,4 @@ sidebar_position: 13

-[Android app project using Split SDK Example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/android-sdk) \ No newline at end of file +[Android app project using FME SDK Example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/android-sdk) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/android-kotlin.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/android-kotlin.md index 48ee8b40d55..63f67ff79ec 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/android-kotlin.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/android-kotlin.md @@ -1,6 +1,6 @@ --- -title: Android Kotlin app project using Split SDK example -sidebar_label: Android Kotlin app project using Split SDK example +title: Android Kotlin app project using FME SDK example +sidebar_label: Android Kotlin app project using FME SDK example helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 7 @@ -10,4 +10,4 @@ sidebar_position: 7

-[Android Kotlin App project using Split SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/Android-Kotlin-Split-SDK) \ No newline at end of file +[Android Kotlin App project using FME SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/Android-Kotlin-Split-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-app.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-app.md index 3f2635d2039..1107119e319 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-app.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-app.md @@ -1,6 +1,6 @@ --- -title: iOS app project using Split SDK example -sidebar_label: iOS app project using Split SDK example +title: iOS app project using FME SDK example +sidebar_label: iOS app project using FME SDK example helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 14 @@ -10,4 +10,4 @@ sidebar_position: 14

-[iOS app project using Split SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/iOS-Swift-SDK) \ No newline at end of file +[iOS app project using FME SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/iOS-Swift-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-obj-c.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-obj-c.md index a65357f29e8..13e4f8511bd 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-obj-c.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-obj-c.md @@ -1,6 +1,6 @@ --- -title: iOS Objective-C project using Split SDK example -sidebar_label: iOS Objective-C project using Split SDK example +title: iOS Objective-C project using FME SDK example +sidebar_label: iOS Objective-C project using FME SDK example helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 3 @@ -10,4 +10,4 @@ sidebar_position: 3

-[iOS Objective-C project using Split SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/iOS-Objective-C-SDK) \ No newline at end of file +[iOS Objective-C project using FME SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/iOS-Objective-C-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-swift-app-two-factories.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-swift-app-two-factories.md index b2a83f90d8b..dd99cebbde6 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-swift-app-two-factories.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/ios-swift-app-two-factories.md @@ -1,6 +1,6 @@ --- -title: iOS Swift app project using two Split SDK Factories example -sidebar_label: iOS Swift app project using two Split SDK Factories example +title: iOS Swift app project using two SDK Factories example +sidebar_label: iOS Swift app project using two SDK Factories example helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 10 @@ -10,4 +10,4 @@ sidebar_position: 10

-[iOS Swift app project using Two Split SDK factories example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/iOS-two-factories-SDK) \ No newline at end of file +[iOS Swift app project using two SDK factories example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/iOS-two-factories-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/javascript-code.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/javascript-code.md index 5ef82420f0d..1f3e9908252 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/javascript-code.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/javascript-code.md @@ -1,6 +1,6 @@ --- -title: JavaScript code using Split SDK example -sidebar_label: JavaScript code using Split SDK example +title: JavaScript code using FME SDK example +sidebar_label: JavaScript code using FME SDK example helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 11 @@ -10,4 +10,4 @@ sidebar_position: 11

-[JavaScript Code using Split SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/JavaScript-SDK) \ No newline at end of file +[JavaScript Code using FME SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/JavaScript-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/nodejs-with-react-redux-using-javascript-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/nodejs-with-react-redux-using-javascript-sdk.md index f65f326bc6d..1b3022c1ec8 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/nodejs-with-react-redux-using-javascript-sdk.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/nodejs-with-react-redux-using-javascript-sdk.md @@ -1,6 +1,6 @@ --- -title: Node.js with React Redux project using Split JavaScript SDK example -sidebar_label: Node.js with React Redux project using Split JavaScript SDK example +title: Node.js with React Redux project using FME JavaScript SDK example +sidebar_label: Node.js with React Redux project using FME JavaScript SDK example helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 12 @@ -10,7 +10,7 @@ sidebar_position: 12

-Example: Basic Code to use JavaScript Split SDK 10.3.3 +Example: Basic Code to use JavaScript SDK 10.3.3 Environment: diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-android-app.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-android-app.md index 35e37ba3f5d..c80a264bee7 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-android-app.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-android-app.md @@ -1,6 +1,6 @@ --- -title: React Native Android app using Split SDK example -sidebar_label: React Native Android app using Split SDK example +title: React Native Android app using FME SDK example +sidebar_label: React Native Android app using FME SDK example helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 9 @@ -10,4 +10,4 @@ sidebar_position: 9

-[React Native Android App using Split SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/React-native-Android-SDK) \ No newline at end of file +[React Native Android App using FME SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/React-native-Android-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-app-nodejs.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-app-nodejs.md index fa311f18e64..5b8ba27a5ad 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-app-nodejs.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-app-nodejs.md @@ -10,7 +10,7 @@ sidebar_position: 4

-Example: Basic example for React Native app project using Split JavaScript SDK +Example: Basic example for React Native app project using JavaScript SDK Example Repo: https://github.com/splitio/react-native-sdk-example @@ -23,7 +23,7 @@ $ cd react-native-sdk-example/ $ npm install --save @splitsoftware/splitio # or 'yarn add @splitsoftware/splitio' if using yarn dependency manager ``` -Additionally, Split SDK can be used with React-Native-CLI. You can take a look at the [React Native getting started guide](https://facebook.github.io/react-native/docs/getting-started.html) if you want to test on your own application. +Additionally, SDK can be used with React-Native-CLI. You can take a look at the [React Native getting started guide](https://facebook.github.io/react-native/docs/getting-started.html) if you want to test on your own application. ``` $ npm install -g react-native-cli @@ -73,10 +73,10 @@ Open it in the [Expo app](https://expo.io/) on your phone to view it. It will re Runs the [jest](https://github.com/facebook/jest) test runner on your tests. :::note -No test cases have been added since this is an example app. If you're looking for how to test with Split SDK, you can: +No test cases have been added since this is an example app. If you're looking for how to test with SDK, you can: * mock the module import, see Jest documentation for that [here](https://facebook.github.io/jest/docs/en/jest-object.html#jestmockmodulename-factory-options) -* use the localhost (offline) mode of the JavaScript Split SDK, more information [here](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#localhost-mode) +* use the localhost (offline) mode of the JavaScript SDK, more information [here](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#localhost-mode) ::: ### `npm run ios` diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-ios-app.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-ios-app.md index 2a4caa50391..9c72741a989 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-ios-app.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/react-native-ios-app.md @@ -1,6 +1,6 @@ --- -title: React Native iOS app using Split SDK example -sidebar_label: React Native iOS app using Split SDK example +title: React Native iOS app using FME SDK example +sidebar_label: React Native iOS app using FME SDK example helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 8 @@ -10,4 +10,4 @@ sidebar_position: 8

-[React Native iOS App using Split SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/React-native-iOS-SDK) \ No newline at end of file +[React Native iOS App using FME SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/React-native-iOS-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/using-split-with-multiple-web-components.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/using-split-with-multiple-web-components.md index 5eb5bfa824e..b01ca8a00a3 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/using-split-with-multiple-web-components.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdk-examples/using-split-with-multiple-web-components.md @@ -1,6 +1,6 @@ --- -title: Using Split with multiple web components and a single factory instance -sidebar_label: Using Split with multiple web components and a single factory instance +title: Using multiple web components and a single SDK factory instance +sidebar_label: Using multiple web components and a single SDK factory instance helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 1 @@ -10,8 +10,8 @@ sidebar_position: 1

-## Using Split SDK in a micro frontend environment -This code example, contributed by Joshua Klein, shows how to employ a shared Split module injected into each of multiple micro frontend JS files. This approach allows for independent development and tooling without having multiple Split factory instances running the in the same browser. +## Using FME SDK in a micro frontend environment +This code example, contributed by Joshua Klein, shows how to employ a shared SDK module injected into each of multiple micro frontend JS files. This approach allows for independent development and tooling without having multiple SDK factory instances running the in the same browser. https://github.com/kleinjoshuaa/Multiple-Web-Components diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/android-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/android-sdk.md index e74282ad251..8748063de65 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/android-sdk.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/android-sdk.md @@ -34,7 +34,7 @@ implementation("androidx.work:work-runtime") { ## Initialization -To get started, set up Split in your code base with the following two steps. +To get started, set up FME in your code base with the following two steps. ### 1. Import the SDK into your project @@ -44,17 +44,17 @@ Import the SDK into your project using the following line: implementation 'io.split.client:android-client:5.1.1' ``` -### 2. Instantiate the SDK and create a new Split client +### 2. Instantiate the SDK and create a new SDK factory client -The first time the SDK is instantiated, it starts background tasks to update an in-memory cache and in-storage cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds, depending on the size of the data. +The first time the SDK is instantiated, it starts background tasks to update an in-memory cache and in-storage cache with small amounts of data fetched from Harness servers. This process can take up to a few hundred milliseconds, depending on the size of the data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while it is in this intermediate state, it may not have the data necessary to run the evaluation. In this circumstance, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072). After the first initialization, the fetched data is stored. Further initializations fetch data from that cache and the configuration is immediately available. -We recommend instantiating the Split factory once as a singleton and reusing it throughout your application. +We recommend instantiating the SDK factory once as a singleton and reusing it throughout your application. -Configure the SDK with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split UI, on your Admin settings page, API keys section. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. +Configure the SDK with the SDK key for the FME environment that you would like to access. The SDK key is available in Harness FME Admin settings. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. @@ -65,7 +65,7 @@ import io.split.android.client.SplitFactory; import io.split.android.client.SplitFactoryBuilder; import io.split.android.client.api.Key; -// Split SDK key +// SDK key String sdkKey = "YOUR_SDK_KEY"; // Build SDK configuration by default @@ -81,7 +81,7 @@ Key key = new Key(matchingKey); // Create factory SplitFactory splitFactory = SplitFactoryBuilder.build(sdkKey, key, config, getApplicationContext()); -// Get Split Client instance +// Get client instance SplitClient client = splitFactory.client(); ``` @@ -93,7 +93,7 @@ import io.split.android.client.SplitFactory import io.split.android.client.SplitFactoryBuilder import io.split.android.client.api.Key -// Split SDK key +// SDK key val sdkKey = "YOUR_SDK_KEY" // Build default SDK configuration @@ -109,7 +109,7 @@ val key: Key = Key(matchingKey) val splitFactory: SplitFactory = SplitFactoryBuilder.build(sdkKey, key, config, applicationContext) -// Get Split Client instance +// Get client instance val client: SplitClient = splitFactory.client() ```
@@ -122,7 +122,7 @@ The following explains how to use this SDK. ### Basic usage -To make sure the SDK is properly loaded before asking it for a treatment, wait until the SDK is ready as shown below. We set the client to listen for the `SDK_READY` event triggered by the SDK before asking for an evaluation. Once the `SDK_READY` event fires, use the `getTreatment` method to return the proper treatment based on the FEATURE_FLAG_NAME you pass and the key you passed when instantiating the SDK. From there, use an if-else-if block as shown below and plug the code in for the different treatments that you defined in the Split user interface. Make sure to remember the final else branch in your code to handle the client returning control. +To make sure the SDK is properly loaded before asking it for a treatment, wait until the SDK is ready as shown below. We set the client to listen for the `SDK_READY` event triggered by the SDK before asking for an evaluation. Once the `SDK_READY` event fires, use the `getTreatment` method to return the proper treatment based on the FEATURE_FLAG_NAME you pass and the key you passed when instantiating the SDK. From there, use an if-else-if block as shown below and plug the code in for the different treatments that you defined in Harness FME. Make sure to remember the final else branch in your code to handle the client returning control. @@ -257,7 +257,7 @@ client.on(SplitEvent.SDK_READY_FROM_CACHE, object : SplitEventTask() { To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK's `getTreatment` methods need to be passed an attribute map at runtime. -In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the Rollout plan as defined in the Split user interface to decide which treatment is assigned to this key. +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the Rollout plan as defined in Harness FME to decide which treatment is assigned to this key. The `getTreatment` method supports five types of attributes: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: @@ -484,7 +484,7 @@ val result = client.clearAttributes() ### Multiple evaluations at once -In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` method of the Split client to do this. +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` method of the SDK factory client to do this. * `getTreatments`: Pass a list of the feature flag names you want treatments for. * `getTreatmentsByFlagSet`: Evaluate all flags that are part of the provided set name and are cached on the SDK instance. * `getTreatmentsByFlagSets`: Evaluate all flags that are part of the provided set names and are cached on the SDK instance. @@ -531,7 +531,7 @@ val treatmentsByFlagSets = client.getTreatmentsByFlagSets(flagSets) To [leverage dynamic configurations with your treatments](https://help.split.io/hc/en-us/articles/360026943552), use the `getTreatmentWithConfig` method. This method returns an object containing the treatment and associated configuration. -The config element is a stringified version of the configuration JSON defined in the Split user interface. If there is no configuration defined for a treatment, the SDK returns `null` for the config parameter. +The config element is a stringified version of the configuration JSON defined in Harness FME. If there is no configuration defined for a treatment, the SDK returns `null` for the config parameter. This method takes the exact same set of arguments as the standard `getTreatment` method. See below for examples on proper usage: @@ -595,7 +595,7 @@ val treatmentsByFlagSets = client.getTreatmentsWithConfigByFlagSets(flagSetNames ### Shutdown -It is good practice to call the `destroy` method before your app shuts down or is destroyed, as this method gracefully shuts down the Split SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. +It is good practice to call the `destroy` method before your app shuts down or is destroyed, as this method gracefully shuts down the SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. @@ -618,18 +618,18 @@ A call to the `destroy()` method also destroys the factory object. When creating ## Track -Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users' actions and metrics. Refer to the [Events](https://help.split.io/hc/en-us/articles/360020585772) guide to learn about using track events in Split. In the examples below, you can see that the `.track()` method can take up to four arguments. The proper data type and syntax for each are: +Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Harness FME and allows you to measure the impact of your feature flags on your users' actions and metrics. Refer to the [Events](https://help.split.io/hc/en-us/articles/360020585772) guide to learn about using track events. In the examples below, you can see that the `.track()` method can take up to four arguments. The proper data type and syntax for each are: -* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined Harness FME. * **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: * Contains 80 characters or fewer. * Starts with a letter or number. * Contains only letters, numbers, hyphen, underscore, or period. * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}` * **VALUE:** (Optional) The value to be used in creating the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is **Float**. -* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. +* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. FME currently supports three types of properties: strings, numbers, and booleans. -The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event to be sent back to Split's servers on the next event post. The SDK will return `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method has been provided. +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event to be sent back to Harness servers on the next event post. The SDK will return `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method has been provided. In case a bad input is provided, refer to the [Track events](https://help.split.io/hc/en-us/articles/360020585772-Track-events) guide for information about our SDK's expected behavior. @@ -726,18 +726,18 @@ The SDK has a number of knobs for configuring performance. Each knob is tuned to | **Configuration** | **Description** | **Default value** | | --- | --- | --- | -| featuresRefreshRate | The SDK polls Split servers for changes to feature flags at this rate (in seconds). | 3600 seconds | -| segmentsRefreshRate | The SDK polls Split servers for changes to segments at this rate (in seconds). | 1800 seconds | -| impressionsRefreshRate | The treatment log captures which customer saw what treatment (on, off, etc.) at what time. This log is periodically flushed back to Split servers. This configuration controls how quickly the cache expires after a write (in seconds). | 1800 seconds | -| telemetryRefreshRate | The SDK caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers (in seconds). | 3600 seconds (1 hour) | +| featuresRefreshRate | The SDK polls Harness servers for changes to feature flags at this rate (in seconds). | 3600 seconds | +| segmentsRefreshRate | The SDK polls Harness servers for changes to segments at this rate (in seconds). | 1800 seconds | +| impressionsRefreshRate | The treatment log captures which customer saw what treatment (on, off, etc.) at what time. This log is periodically flushed back to Harness servers. This configuration controls how quickly the cache expires after a write (in seconds). | 1800 seconds | +| telemetryRefreshRate | The SDK caches diagnostic data that it periodically sends to Harness servers. This configuration controls how frequently this data is sent back to Harness servers (in seconds). | 3600 seconds (1 hour) | | eventsQueueSize | When using `.track`, the number of **events** to be kept in memory. | 10000 | -| eventFlushInterval | When using `.track`, how often is the events queue flushed to Split's servers. | 1800 seconds | +| eventFlushInterval | When using `.track`, how often is the events queue flushed to Harness servers. | 1800 seconds | | eventsPerPush | Maximum size of the batch to push events. | 2000 | | trafficType | When using `.track`, the default traffic type to be used. | not set | | connectionTimeout | HTTP client connection timeout (in ms). | 10000 ms | | readTimeout | HTTP socket read timeout (in ms). | 10000 ms | | impressionsQueueSize | Default queue size for impressions. | 30K | -| disableLabels | Disable labels from being sent to Split backend. Labels may contain sensitive information. | true | +| disableLabels | Disable labels from being sent to Harness servers. Labels may contain sensitive information. | true | | logLevel | Enables logging according to the level specified. Options are `NONE`, `VERBOSE`, `DEBUG`, `INFO`, `WARNING`, `ERROR`, and `ASSERT`. | `NONE` | | proxyHost | The location of the proxy using standard URI: `scheme://user:password@domain:port/path`. If no port is provided, the SDK defaults to port 80. | null | | ready | Maximum amount of time in milliseconds to wait before notifying a timeout. | -1 (not set) | @@ -748,8 +748,8 @@ The SDK has a number of knobs for configuring performance. Each knob is tuned to | streamingEnabled | Boolean flag to enable the streaming service as default synchronization mechanism when in foreground. In the event of an issue with streaming, the SDK will fallback to the polling mechanism. If false, the SDK will poll for changes as usual without attempting to use streaming. | true | | syncConfig | Optional SyncConfig instance. Use it to filter specific feature flags to be synced and evaluated by the SDK. These filters can be created with the `SplitFilter::bySet` static function (recommended, flag sets are available in all tiers), or `SplitFilter::byName` static function, and appended to this config using the `SyncConfig` builder. If not set or empty, all feature flags are downloaded by the SDK. | null | | persistentAttributesEnabled | Enables saving attributes on persistent cache which is loaded as part of the SDK_READY_FROM_CACHE flow. All functions that mutate the stored attributes map affect the persistent cache. | false | -| syncEnabled | Controls the SDK continuous synchronization flags. When `true`, a running SDK processes rollout plan updates performed in the Split user interface (default). When `false`, it fetches all data upon init, which ensures a consistent experience during a user session and optimizes resources when these updates are not consumed by the app. | true | -| impressionsMode | This configuration defines how impressions (decisioning events) are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Split; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, ALL impressions are queued and sent to Split; this is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. | `OPTIMIZED` | +| syncEnabled | Controls the SDK continuous synchronization flags. When `true`, a running SDK processes rollout plan updates performed in Harness FME (default). When `false`, it fetches all data upon init, which ensures a consistent experience during a user session and optimizes resources when these updates are not consumed by the app. | true | +| impressionsMode | This configuration defines how impressions (decisioning events) are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Harness; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Harness FME and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, ALL impressions are queued and sent to Harness; this is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. | `OPTIMIZED` | | userConsent | User consent status used to control the tracking of events and impressions. Possible values are `GRANTED`, `DECLINED`, and `UNKNOWN`. See [User consent](#user-consent) for details. | `GRANTED` | | encryptionEnabled | If set to `true`, the local database contents is encrypted. | false | | prefix | If set, the prefix will be prepended to the database name used by the SDK. | null | @@ -774,7 +774,7 @@ SplitClientConfig config = SplitClientConfig.builder() .build()) .build(); -// Split SDK key +// SDK key String sdkKey = "YOUR_SDK_KEY"; // Create a new user key to be evaluated @@ -785,7 +785,7 @@ Key k = new Key(matchingKey,bucketingKey); // Create factory SplitFactory splitFactory = SplitFactoryBuilder.build(sdkKey, k, config, getApplicationContext()); -// Get Split Client instance +// Get client instance SplitClient client = splitFactory.client(); ``` @@ -819,7 +819,7 @@ val client: SplitClient = splitFactory.client() ## Localhost mode -For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, you can start the Split SDK in **localhost** mode (aka, off-the-grid mode). In this mode, the SDK neither polls or updates Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the feature flags. To use the SDK in localhost mode, replace the API Key with `localhost`, as shown in the example below: +For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, you can start the SDK in **localhost** mode (aka, off-the-grid mode). In this mode, the SDK neither polls or updates Harness servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the feature flags. To use the SDK in localhost mode, replace the API Key with `localhost`, as shown in the example below: Since version 2.2.0, our SDK supports a new type of localhost feature flag definition file, using the YAML format. This new format allows the user to map different keys to different treatments within a single feature flag, and also add configurations to them. The new format is a list of single-key maps (one per mapping feature_flag-keys-config), defined as follows: @@ -883,7 +883,7 @@ val client = SplitFactoryBuilder.build( -If a split.yaml or split.yml is not found in assets, Split SDK maintains backward compatibility by trying to load the legacy file (split.properties), which is now deprecated. +If a split.yaml or split.yml is not found in assets, the SDK maintains backward compatibility by trying to load the legacy file (split.properties), which is now deprecated. The format of this file is a properties file as key-value line. The key is the feature flag name, and the value is the treatment name. The following is a sample `split.properties` file: @@ -900,7 +900,7 @@ new-navigation=v3 ## Manager -Use the Split Manager to get a list of feature flags available to the Split client. To instantiate a Manager in your code base, use the same factory that you used for your client. +Use the Split Manager to get a list of feature flags available to the SDK factory client. To instantiate a Manager in your code base, use the same factory that you used for your client. @@ -945,7 +945,7 @@ SplitView split(String SplitName); /** * Returns the names of feature flags registered with the SDK. * - * @return a List of String (Split feature names) or empty + * @return a List of String (Feature flag names) or empty */ List splitNames(); ``` @@ -1013,7 +1013,7 @@ class SplitView( ## Listener -Split SDKs send impression data back to Split servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression listener*. +FME SDKs send impression data back to Harness servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression listener*. The SDK sends the generated impressions to the impression listener right away. Because of this, be careful while implementing handling logic to avoid blocking the main thread. Generally speaking, you should create a separate thread to handle incoming impressions. Refer to the snippet below: @@ -1108,7 +1108,7 @@ In regards with the data available here, refer to the `Impression` objects inter ## Flush -The flush() method sends the data stored in memory (impressions and events) to the Split cloud and clears the successfully posted data. If a connection issue is experienced, the data is sent on the next attempt. If you want to flush all pending data when your app goes to background, a good place to call this method is the onPause callback of your MainActivity. +The flush() method sends the data stored in memory (impressions and events) to the Harness FME servers and clears the successfully posted data. If a connection issue is experienced, the data is sent on the next attempt. If you want to flush all pending data when your app goes to background, a good place to call this method is the onPause callback of your MainActivity. @@ -1149,7 +1149,7 @@ This section describes advanced use cases and features provided by the SDK. ### Instantiate multiple SDK clients -In versions previous to 2.10.0, you had to create more that one SDK instance to evaluate for different users IDs. From 2.10.0 on, Split supports the ability to create multiple clients, one for each user ID. For example, if you need to roll out feature flags for different user IDs, you can instantiate multiple clients, one for each ID. You can then evaluate them using the corresponding client. +In versions previous to 2.10.0, you had to create more that one SDK instance to evaluate for different users IDs. From 2.10.0 on, FME supports the ability to create multiple clients, one for each user ID. For example, if you need to roll out feature flags for different user IDs, you can instantiate multiple clients, one for each ID. You can then evaluate them using the corresponding client. You can do this using the example below: @@ -1227,9 +1227,9 @@ While the SDK does not put any limitations on the number of instances that can b You can listen for four different events from the SDK. * `SDK_READY_FROM_CACHE`. This event fires once the SDK is ready to evaluate treatments using a locally cached version of your rollout plan from a previous session (which might be stale). If there is data in the cache, this event fires almost immediately, since access to the cache is fast; otherwise, it doesn't fire. -* `SDK_READY`. This event fires once the SDK is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Split servers. -* `SDK_READY_TIMED_OUT `. This event fires if there is no cached version of your rollout plan in disk cache, and the SDK could not fully download the data from Split servers within the time specified by the `ready` setting of the `SplitClientConfig` object. This event does not indicate that the SDK initialization was interrupted. The SDK continues downloading the rollout plan and fires the `SDK_READY` event when finished. This delayed `SDK_READY` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. -* `SDK_UPDATE`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in the Split user interface. +* `SDK_READY`. This event fires once the SDK is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Harness servers. +* `SDK_READY_TIMED_OUT `. This event fires if there is no cached version of your rollout plan in disk cache, and the SDK could not fully download the data from Harness servers within the time specified by the `ready` setting of the `SplitClientConfig` object. This event does not indicate that the SDK initialization was interrupted. The SDK continues downloading the rollout plan and fires the `SDK_READY` event when finished. This delayed `SDK_READY` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. +* `SDK_UPDATE`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in Harness FME. An event is an extension of a SplitEventTask. @@ -1374,8 +1374,8 @@ The SDK allows you to disable the tracking of events and impressions until user The `userConsent` configuration parameter lets you set the initial consent status of the SDK instance, and the factory method `setUserConsent(enabled: Bool)` lets you grant (enable) or decline (disable) dynamic data tracking. There are three possible initial states: - * `'GRANTED'`: The user grants consent for tracking events and impressions. The SDK sends them to Split cloud. This is the default value if `userConsent` param is not defined. - * `'DECLINED'`: The user declines consent for tracking events and impressions. The SDK does not send them to Split cloud. + * `'GRANTED'`: The user grants consent for tracking events and impressions. The SDK sends them to Harness FME servers. This is the default value if `userConsent` param is not defined. + * `'DECLINED'`: The user declines consent for tracking events and impressions. The SDK does not send them to Harness FME servers. * `'UNKNOWN'`: The user neither grants nor declines consent for tracking events and impressions. The SDK tracks them in its internal storage, and eventually either sends them or not if the consent status is updated to `'GRANTED'` or `'DECLINED'` respectively. The status can be updated at any time with the `setUserConsent` factory method. @@ -1385,7 +1385,7 @@ Working with user consent is demonstrated below. ```java title="User consent: Initial config, getter and setter" // Overwrites the initial consent status of the factory instance, which is 'GRANTED' by default. // 'UNKNOWN' status represents that the user has neither granted nor declined consent for tracking data, -// so the SDK locally tracks data but not send it to Split cloud until consent is changed to 'GRANTED'. +// so the SDK locally tracks data but not send it to Harness FME servers until consent is changed to 'GRANTED'. SplitClientConfig config = SplitClientConfig.builder() .userConsent(UserConsent.UNKNOWN) .build(); @@ -1395,9 +1395,9 @@ try { new Key(mUserKey, null), config, context); - // Changed User Consent status to 'GRANTED'. Data is sent to Split cloud. + // Changed User Consent status to 'GRANTED'. Data is sent to Harness FME servers. factory.setUserConsent(true); - // Changed User Consent status to 'DECLINED'. Data is not sent to Split cloud. + // Changed User Consent status to 'DECLINED'. Data is not sent to Harness FME servers. factory.setUserConsent(false); // The 'getUserConsent' method returns User Consent status. @@ -1452,7 +1452,7 @@ CertificatePinningConfiguration certPinningConfig = CertificatePinningConfigurat .build(); -// Set the CertificatePinningConfiguration property for the Split client configuration +// Set the CertificatePinningConfiguration property for the SDK factory client configuration SplitClientConfig config = SplitClientConfig.builder() .certificatePinningConfiguration(certPinningConfig) // you can add other configuration properties here @@ -1481,7 +1481,7 @@ val certPinningConfig = CertificatePinningConfiguration.builder() .build() -// Set the CertificatePinningConfiguration property for the Split client configuration +// Set the CertificatePinningConfiguration property for the SDK factory client configuration val config = SplitClientConfig.builder() .certificatePinningConfiguration(certPinningConfig) // you can add other configuration properties here diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/angular-utilities.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/angular-utilities.md index 9cfd029e3c1..4f420547a0c 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/angular-utilities.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/angular-utilities.md @@ -22,7 +22,7 @@ These utilities guarantee support with Angular v15.2.10 or later. ## Initialization -Set up Split in your code base with the following two steps: +Set up FME in your code base with the following two steps: ### 1. Import the utilities into your project @@ -68,19 +68,19 @@ Feel free to access the declaration files if IntelliSense is not enough. We recommend instantiating the service once as a singleton and reusing it throughout your application. -Configure the service with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split UI, on your Admin settings page, API keys section. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. +Configure the service with the SDK key for the FME environment that you would like to access. The SDK key is available in Harness FME Admin settings. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. ## Using the service ### Basic use -When the SDK is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while its in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). +When the SDK is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Harness servers. This process can take up to a few hundred milliseconds depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while its in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). To make sure the SDK is properly loaded before asking it for a treatment, block until the SDK is ready, as shown below. You can subscribe to `splitService.sdkReady$` observable provided by splitService before asking for an evaluation. After the observable calls back, you can use the `getTreatment` method to return the proper treatment based on the `FEATURE_FLAG_NAME` and the `key` variable you passed when instantiating the SDK. -Then use an if-else-if block as shown below and insert the code for the different treatments that you defined in the Split user interface. Remember the final else branch in your code to handle the client returning control. +Then use an if-else-if block as shown below and insert the code for the different treatments that you defined in Harness FME. Remember the final else branch in your code to handle the client returning control. ```javascript title="TypeScript" this.splitService.sdkReady$.subscribe(() => { @@ -100,7 +100,7 @@ this.splitService.sdkReady$.subscribe(() => { To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the splitService's `getTreatment` method needs to be passed an attribute map at runtime. -In the example below, we are rolling out a feature to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in the Split user interface to decide whether to show the `on` or `off` treatment to this account. +In the example below, we are rolling out a feature to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in Harness FME to decide whether to show the `on` or `off` treatment to this account. The `getTreatment` method supports five types of attributes: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: @@ -137,7 +137,7 @@ You can pass your attributes in exactly this way to the `splitService.getTreatme ### Multiple evaluations at once -In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` from the Split client to do this. +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` from the SDK factory client to do this. * `getTreatments`: Pass a list of the feature flag names you want treatments for. * `getTreatmentsByFlagSet`: Evaluate all flags that are part of the provided set name and are cached on the SDK instance. * `getTreatmentsByFlagSets`: Evaluates all flags that are part of the provided set names and are cached on the SDK instance. @@ -194,7 +194,7 @@ type TreatmentResult = { }; ``` -From the object structure, the config is a stringified version of the configuration JSON defined in the Split user interface. If no configuration is defined for a treatment, the SDK returns `null` for the config parameter. This method takes the same set of arguments as the standard `getTreatment` method. Refer to the examples below for proper usage: +From the object structure, the config is a stringified version of the configuration JSON defined in Harness FME. If no configuration is defined for a treatment, the SDK returns `null` for the config parameter. This method takes the same set of arguments as the standard `getTreatment` method. Refer to the examples below for proper usage: ```javascript title="TypeScript" const treatmentResult: SplitIO.TreatmentWithConfig = this.splitService.getTreatmentWithConfig('FEATURE_FLAG_NAME', attributes); @@ -260,7 +260,7 @@ const treatmentResults: SplitIO.TreatmentsWithConfig = this.splitService.getTrea ### Shutdown -Call the `splitService.destroy()` method before letting a process using the SDK exit, as this method gracefully shuts down the Split SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. +Call the `splitService.destroy()` method before letting a process using the SDK exit, as this method gracefully shuts down the SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. ```javascript title="TypeScript" // You can just destroy and remove the variable reference and move on: @@ -285,20 +285,20 @@ A call to the `destroy()` method also destroys the splitService object. When cre ## Track -Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your features on your users' actions and metrics. [Learn more about using track events](https://help.split.io/hc/en-us/articles/360020585772) in Split. +Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Harness FME and allows you to measure the impact of your features on your users' actions and metrics. [Learn more about using track events](https://help.split.io/hc/en-us/articles/360020585772). In the examples below, you can see that the `.track()` method can take up to four arguments. The proper data type and syntax for each are: -* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined Harness FME. * **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: * Contains 63 characters or fewer. * Starts with a letter or number. * Contains only letters, numbers, hyphen, underscore, or period. * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` * **VALUE:** (Optional) The value is used to create the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is **Integer** or **Float**. -* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. +* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. FME currently supports three types of properties: strings, numbers, and booleans. -The `track` method returns a boolean value of `true` or `false` to indicate whether or not the splitService was able to successfully queue the event to be sent back to Split's servers on the next event post. The service returns `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method has been provided. +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the splitService was able to successfully queue the event to be sent back to Harness servers on the next event post. The service returns `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method has been provided. In the case that a bad input is provided, you can read more about our [SDK's expected behavior](https://help.split.io/hc/en-us/articles/360020585772-Track-events) @@ -321,7 +321,7 @@ The SDK has a number of knobs for configuring performance. Each knob is tuned to ## Manager -To get a list of features available to the Split client, you can use the methods available on splitService as shown below: +To get a list of features available to the SDK factory client, you can use the methods available on splitService as shown below: ```javascript title="TypeScript" import { SplitService } from '@splitsoftware/splitio-angular'; @@ -371,7 +371,7 @@ type SplitView = { ## Listener -Split SDKs send impression data back to Split servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression listener*. For this purpose, the SDK's configurations have a parameter called `impressionListener` where an implementation of `ImpressionListener` could be added. This implementation **must** define the `logImpression` method and it receives data in the following schema. +FME SDKs send impression data back to Harness servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression listener*. For this purpose, the SDK's configurations have a parameter called `impressionListener` where an implementation of `ImpressionListener` could be added. This implementation **must** define the `logImpression` method and it receives data in the following schema. | **Name** | **Type** | **Description** | | --- | --- | --- | @@ -445,9 +445,9 @@ This section describes advanced use cases and features provided by the SDK. ### Instantiate multiple SDK clients -Split supports the ability to release based on multiple traffic types. For example, with traffic types, you can release to `users` in one feature flag and `accounts` in another. If you are unfamiliar with using multiple traffic types, refer to the [Traffic type guide](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) for more information. +FME supports the ability to release based on multiple traffic types. For example, with traffic types, you can release to `users` in one feature flag and `accounts` in another. If you are unfamiliar with using multiple traffic types, refer to the [Traffic type guide](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) for more information. -Each SDK client is tied to one specific customer ID at a time, so if you need to roll out features by different traffic types, instantiate multiple SDK clients, one for each traffic type. For example, you may want to roll out the feature `user-poll` by `users` and the feature `account-permissioning` by `accounts`. +Each SDK factory client is tied to one specific customer ID at a time, so if you need to roll out features by different traffic types, instantiate multiple SDK clients, one for each traffic type. For example, you may want to roll out the feature `user-poll` by `users` and the feature `account-permissioning` by `accounts`. You can do this with the example below: @@ -495,9 +495,9 @@ While the SDK does not put any limitations on the number of instances that can b You can subscribe to four different observables of the splitService. * `sdkReadyFromCache$`. This event fires once the SDK is ready to evaluate treatments using a version of your rollout plan cached in localStorage from a previous session (which might be stale). If there is data in localStorage, this event fires almost immediately, since access to localStorage is fast; otherwise, it doesn't fire. -* `sdkReady$`. This event fires once the SDK is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Split servers. -* `sdkReadyTimedOut$`. This event fires if there is no cached version of your rollout plan cached in localStorage, and the SDK could not download the data from Split servers within the time specified by the `readyTimeout` configuration parameter. This event does not indicate that the SDK initialization was interrupted. The SDK continues downloading the rollout plan and fires the `sdkReady$` event when finished. This delayed `sdkReady$` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. -* `sdkUpdate$`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in the Split user interface. +* `sdkReady$`. This event fires once the SDK is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Harness servers. +* `sdkReadyTimedOut$`. This event fires if there is no cached version of your rollout plan cached in localStorage, and the SDK could not download the data from Harness servers within the time specified by the `readyTimeout` configuration parameter. This event does not indicate that the SDK initialization was interrupted. The SDK continues downloading the rollout plan and fires the `sdkReady$` event when finished. This delayed `sdkReady$` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. +* `sdkUpdate$`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in Harness FME. The syntax to subscribe for each Observable is shown below: @@ -531,9 +531,9 @@ this.splitService.sdkUpdate$.subscribe(() => { console.log('The SDK has been updated!'); } -// This event will fire only using the LocalStorage option and if there's Split data stored in the browser. +// This event will fire only using the LocalStorage option and if there's FME data stored in the browser. this.splitService.sdkReadyFromCache$.subscribe(() => { - // Fired after the SDK could confirm the presence of the Split data. + // Fired after the SDK could confirm the presence of the FME data. // This event fires really quickly, since there's no actual fetching of information. // Keep in mind that data might be stale, this is NOT a replacement of sdkReady. } @@ -564,6 +564,6 @@ export class AppRoutingModule { } ## Example apps -The following are example applications detailing how to configure and instantiate the Split Angular utilities on commonly used platforms. +The following are example applications detailing how to configure and instantiate the FME Angular utilities on commonly used platforms. * [Angular](https://github.com/splitio/angular-sdk-examples) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/browser-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/browser-sdk.md index ecd21a6799e..414beb1c45f 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/browser-sdk.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/browser-sdk.md @@ -38,7 +38,7 @@ If you're looking for possible polyfill options, check [es6-promise](https://git ## Initialization -Set up Split in your code base with the following two steps: +Set up FME in your code base with the following two steps: ### 1. Import the SDK into your project @@ -66,7 +66,7 @@ including fetch polyfill --> -### 2. Instantiate the SDK and create a new Split client +### 2. Instantiate the SDK and create a new SDK factory client @@ -140,21 +140,21 @@ With the SDK package on NPM, you get the SplitIO namespace, which contains usefu Feel free to dive into the declaration files if IntelliSense is not enough. ::: -We recommend instantiating the Split factory once as a singleton and reusing it throughout your application. +We recommend instantiating the SDK factory once as a singleton and reusing it throughout your application. -Configure the SDK with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split UI, on your Admin settings page, API keys section. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. +Configure the SDK with the SDK key for the FME environment that you would like to access. The SDK key is available in Harness FME Admin settings. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. ## Using the SDK ### Basic use -When the SDK is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while its in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). +When the SDK is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Harness servers. This process can take up to a few hundred milliseconds depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while its in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). To make sure the SDK is properly loaded before asking it for a treatment, block until the SDK is ready, as shown below. We set the client to listen for the `SDK_READY` event triggered by the SDK before asking for an evaluation. After the `SDK_READY` event fires, you can use the `getTreatment` method to return the proper treatment based on the `FEATURE_FLAG_NAME` and the `key` variables you passed when instantiating the SDK. -Then use an if-else-if block as shown below and insert the code for the different treatments that you defined in the Split user interface. Remember the final else branch in your code to handle the client returning control. +Then use an if-else-if block as shown below and insert the code for the different treatments that you defined in Harness FME. Remember the final else branch in your code to handle the client returning control. @@ -193,7 +193,7 @@ client.on(client.Event.SDK_READY, function() { To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK's `getTreatment` method needs to be passed an attribute map at runtime. -In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in the Split user interface to decide whether to show the `on` or `off` treatment to this account. +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in Harness FME to decide whether to show the `on` or `off` treatment to this account. The `getTreatment` method has a number of variations that are described below. Each of these additionally has a variation that takes an attributes argument, which can defines attributes of the following types: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: @@ -302,7 +302,7 @@ var result = client.clearAttributes(); ### Multiple evaluations at once -In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` from the Split client to do this. +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` from the SDK factory client to do this. * `getTreatments`: Pass a list of the feature flag names you want treatments for. * `getTreatmentsByFlagSet`: Evaluate all flags that are part of the provided set name and are cached on the SDK instance. * `getTreatmentsByFlagSets`: Evaluate all flags that are part of the provided set names and are cached on the SDK instance. @@ -363,7 +363,7 @@ type TreatmentResult = { }; ``` -As you can see from the object structure, the config is a stringified version of the configuration JSON defined in the Split user interface. If there is no configuration defined for a treatment, the SDK returns `null` for the config parameter. This method takes the exact same set of arguments as the standard `getTreatment` method. See below for examples on proper usage: +As you can see from the object structure, the config is a stringified version of the configuration JSON defined in Harness FME. If there is no configuration defined for a treatment, the SDK returns `null` for the config parameter. This method takes the exact same set of arguments as the standard `getTreatment` method. See below for examples on proper usage: @@ -449,7 +449,7 @@ treatmentResults = client.getTreatmentsWithConfigByFlagSets(flagSets); ### Shutdown -Call the `client.destroy()` method before letting a process using the SDK exit, as this method gracefully shuts down the Split SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. +Call the `client.destroy()` method before letting a process using the SDK exit, as this method gracefully shuts down the SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. @@ -478,20 +478,20 @@ A call to the `destroy()` method also destroys the factory object. When creating ## Track -Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users’ actions and metrics. [Learn more about using track events](https://help.split.io/hc/en-us/articles/360020585772) in Split. +Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Harness FME and allows you to measure the impact of your feature flags on your users’ actions and metrics. [Learn more about using track events](https://help.split.io/hc/en-us/articles/360020585772). In the examples below, you can see that the `.track()` method can take up to four arguments. The proper data type and syntax for each are: -* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined Harness FME. * **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: * Contains 63 characters or fewer. * Starts with a letter or number. * Contains only letters, numbers, hyphen, underscore, or period. * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` * **VALUE:** (Optional) The value to be used in creating the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is **Integer** or **Float**. -* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. +* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. FME currently supports three types of properties: strings, numbers, and booleans. -The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event to be sent back to Split's servers on the next event post. The SDK returns `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method has been provided. +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event to be sent back to Harness servers on the next event post. The SDK returns `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method has been provided. In the case that a bad input has been provided, you can read more about our [SDK's expected behavior](https://help.split.io/hc/en-us/articles/360020585772-Track-events) @@ -532,21 +532,21 @@ The SDK has a number of knobs for configuring performance. Each knob is tuned to | **Configuration** | **Description** | **Default value** | | --- | --- | --- | -| core.labelsEnabled | Enable impression labels from being sent to Split cloud. Labels may contain sensitive information. | true | +| core.labelsEnabled | Enable impression labels from being sent to Harness FME servers. Labels may contain sensitive information. | true | | startup.readyTimeout | Maximum amount of time in seconds to wait before firing the `SDK_READY_TIMED_OUT` event | 10 | | startup.requestTimeoutBeforeReady | The SDK has two main endpoints it uses /splitChanges and /mySegments that it hits to get ready. This config sets how long (in seconds) the SDK will wait for each request it makes as part of getting ready. | 5 | | startup.retriesOnFailureBeforeReady | How many retries on /splitChanges and /mySegments we will do while getting the SDK ready | 1 | | startup.eventsFirstPushWindow | Use to set a specific timer (expressed in seconds) for the first push of events, starting on SDK initialization. | 10 | -| scheduler.featuresRefreshRate | The SDK polls Split servers for changes to feature rollout plans. This parameter controls this polling period in seconds. | 60 | -| scheduler.segmentsRefreshRate | The SDK polls Split servers for changes to segment definitions. This parameter controls this polling period in seconds. | 60 | -| scheduler.impressionsRefreshRate | The SDK sends information on who got what treatment at what time back to Split servers to power analytics. This parameter controls how often this data is sent to Split servers. The parameter should be in seconds. | 300 | +| scheduler.featuresRefreshRate | The SDK polls Harness servers for changes to feature rollout plans. This parameter controls this polling period in seconds. | 60 | +| scheduler.segmentsRefreshRate | The SDK polls Harness servers for changes to segment definitions. This parameter controls this polling period in seconds. | 60 | +| scheduler.impressionsRefreshRate | The SDK sends information on who got what treatment at what time back to Harness servers to power analytics. This parameter controls how often this data is sent to Harness servers. The parameter should be in seconds. | 300 | | scheduler.impressionsQueueSize | The max amount of impressions we queue. If the queue is full, the SDK flushes the impressions and resets the timer. | 30000 | -| scheduler.eventsPushRate | The SDK sends tracked events to Split servers. This setting controls that flushing rate in seconds. | 60 | +| scheduler.eventsPushRate | The SDK sends tracked events to Harness servers. This setting controls that flushing rate in seconds. | 60 | | scheduler.eventsQueueSize | The max amount of events we queue. If the queue is full, the SDK flushes the events and resets the timer. | 500 | -| scheduler.telemetryRefreshRate | The SDK caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers (in seconds). | 3600 seconds (1 hour) | +| scheduler.telemetryRefreshRate | The SDK caches diagnostic data that it periodically sends to Harness servers. This configuration controls how frequently this data is sent back to Harness servers (in seconds). | 3600 seconds (1 hour) | | sync.splitFilters | Filter specific feature flags to be synced and evaluated by the SDK. This is formed by a type string property and a list of string values for the given criteria. Using the types 'bySet' (recommended, flag sets are available in all tiers) or 'byName', pass an array of strings defining the query. If empty or unset, all feature flags are downloaded by the SDK. | [] | -| sync.impressionsMode | This configuration defines how impressions (decisioning events) are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Split; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, ALL impressions are queued and sent to Split; this is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. | `OPTIMIZED` | -| sync.enabled | Controls the SDK continuous synchronization flags. When `true`, a running SDK processes rollout plan updates performed in the Split user interface (default). When `false`, it fetches all data upon init, which ensures a consistent experience during a user session and optimizes resources when these updates are not consumed by the app. | true | +| sync.impressionsMode | This configuration defines how impressions (decisioning events) are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Harness; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Harness FME and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, ALL impressions are queued and sent to Harness; this is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. | `OPTIMIZED` | +| sync.enabled | Controls the SDK continuous synchronization flags. When `true`, a running SDK processes rollout plan updates performed in Harness FME (default). When `false`, it fetches all data upon init, which ensures a consistent experience during a user session and optimizes resources when these updates are not consumed by the app. | true | | sync.requestOptions.getHeaderOverrides | A callback function that can be used to override the Authentication header or append new headers to the SDK's HTTP(S) requests. | undefined | | storage | Pluggable storage instance to be used by the SDK as a complement to in memory storage. Only supported option today is `InLocalStorage`. Read more [here](#configuring-localstorage-cache-for-the-sdk). | In memory storage | | debug | Either a boolean flag, string log level or logger instance for activating SDK logs. See [logging](#logging) for details. | false | @@ -672,7 +672,7 @@ const client = factory.client(); ## Localhost mode -For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the Split SDK can be started in **localhost** mode (aka off-the-grid or offline mode). In this mode, the SDK neither polls nor updates Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the features. +For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the SDK can be started in **localhost** mode (aka off-the-grid or offline mode). In this mode, the SDK neither polls nor updates Harness servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the features. Define the feature flags you want to use in the `features` object map. All `getTreatment` calls for a feature flag now only return the one treatment (and config, if defined) that you have defined in the map. Any feature that is not provided in the `features` map returns the [control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment) if the SDK was asked to evaluate them. @@ -770,7 +770,7 @@ config.features = { 'reporting_v3': 'off' }; // Will not emit SDK_UPDATE ## Manager -Use the Split Manager to get a list of features available to the Split client. To instantiate a Manager in your code base, use the same factory that you used for your client: +Use the Split Manager to get a list of features available to the SDK factory client. To instantiate a Manager in your code base, use the same factory that you used for your client: @@ -778,11 +778,6 @@ Use the Split Manager to get a list of features available to the Split client. T var factory = SplitFactory({ core: { authorizationKey: 'YOUR_SDK_KEY', - // the key can be the logged in - // user id, or the account id that - // the logged in user belongs to. - // The type of customer (user, account, custom) - // is chosen during Split's sign-up process. key: 'key' } }); @@ -799,11 +794,6 @@ manager.once(manager.Event.SDK_READY, function() { const factory: SplitIO.IBrowserSDK = SplitFactory({ core: { authorizationKey: 'YOUR_SDK_KEY', - // the key can be the logged in - // user id, or the account id that - // the logged in user belongs to. - // The type of customer (user, account, custom) - // is chosen during Split's sign-up process. key: 'key' } }); @@ -890,7 +880,7 @@ type SplitView = { ## Listener -Split SDKs send impression data back to Split servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression listener*. For that purpose, the SDK's configurations have a parameter called `impressionListener` where an implementation of `ImpressionListener` could be added. This implementation **must** define the `logImpression` method and it receives data in the following schema. +FME SDKs send impression data back to Harness servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression listener*. For that purpose, the SDK's configurations have a parameter called `impressionListener` where an implementation of `ImpressionListener` could be added. This implementation **must** define the `logImpression` method and it receives data in the following schema. | **Name** | **Type** | **Description** | | --- | --- | --- | @@ -993,7 +983,7 @@ var sdk = splitio.SplitFactory({ -You can also enable the SDK logging via a boolean or log level value as `debug` settings, and change it dynamically by calling the SDK Logger API. However, in any case where the proper logger instance is not plugged in, instead of a human readable message, you'll get a code and optionally some params for the log itself. While these logs would be enough for the Split support team, if you find yourself in a scenario where you need to parse this information, you can check the constant files in our javascript-commons repository (where you have tags per version if needed) under the [logger folder](https://github.com/splitio/javascript-commons/blob/master/src/logger/). +You can also enable the SDK logging via a boolean or log level value as `debug` settings, and change it dynamically by calling the SDK Logger API. However, in any case where the proper logger instance is not plugged in, instead of a human readable message, you'll get a code and optionally some params for the log itself. While these logs would be enough for the Harness FME support team, if you find yourself in a scenario where you need to parse this information, you can check the constant files in our javascript-commons repository (where you have tags per version if needed) under the [logger folder](https://github.com/splitio/javascript-commons/blob/master/src/logger/). @@ -1048,9 +1038,9 @@ This section describes advanced use cases and features provided by the SDK. ### Instantiate multiple SDK clients -Split supports the ability to release based on multiple traffic types. For example, with traffic types, you can release to `users` in one feature flag and `accounts` in another. If you are unfamiliar with using multiple traffic types, refer to the [Traffic type guide](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) for more information. +FME supports the ability to release based on multiple traffic types. For example, with traffic types, you can release to `users` in one feature flag and `accounts` in another. If you are unfamiliar with using multiple traffic types, refer to the [Traffic type guide](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) for more information. -Each SDK client is tied to one specific customer ID at a time, so if you need to roll out feature flags by different traffic types, instantiate multiple SDK clients, one for each traffic type. For example, you may want to roll out the feature `user-poll` by `users` and the feature `account-permissioning` by `accounts`. +Each SDK factory client is tied to one specific customer ID at a time, so if you need to roll out feature flags by different traffic types, instantiate multiple SDK clients, one for each traffic type. For example, you may want to roll out the feature `user-poll` by `users` and the feature `account-permissioning` by `accounts`. You can do this with the example below: @@ -1138,9 +1128,9 @@ While the SDK does not put any limitations on the number of instances that can b You can listen for four different events from the SDK. * `SDK_READY_FROM_CACHE`. This event fires once the SDK is ready to evaluate treatments using a version of your rollout plan cached in localStorage from a previous session (which might be stale). If there is data in localStorage, this event fires almost immediately, since access to localStorage is fast; otherwise, it doesn't fire. -* `SDK_READY`. This event fires once the SDK is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Split servers. -* `SDK_READY_TIMED_OUT`. This event fires if there is no cached version of your rollout plan cached in localStorage, and the SDK could not download the data from Split servers within the time specified by the `readyTimeout` configuration parameter. This event does not indicate that the SDK initialization was interrupted. The SDK continues downloading the rollout plan and fires the `SDK_READY` event when finished. This delayed `SDK_READY` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. -* `SDK_UPDATE`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in the Split user interface. +* `SDK_READY`. This event fires once the SDK is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Harness servers. +* `SDK_READY_TIMED_OUT`. This event fires if there is no cached version of your rollout plan cached in localStorage, and the SDK could not download the data from Harness servers within the time specified by the `readyTimeout` configuration parameter. This event does not indicate that the SDK initialization was interrupted. The SDK continues downloading the rollout plan and fires the `SDK_READY` event when finished. This delayed `SDK_READY` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. +* `SDK_UPDATE`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in Harness FME. The syntax to listen for each event is shown below: @@ -1173,9 +1163,9 @@ client.on(client.Event.SDK_UPDATE, function () { console.log('The SDK has been updated!'); }); -// This event fires only using the LocalStorage option and if there's Split data stored in the browser. +// This event fires only using the LocalStorage option and if there's FME data stored in the browser. client.once(client.Event.SDK_READY_FROM_CACHE, function () { - // Fired after the SDK could confirm the presence of the Split data. + // Fired after the SDK could confirm the presence of the FME data. // This event fires really quickly, since there's no actual fetching of information. // Keep in mind that data might be stale, this is NOT a replacement of SDK_READY. }); @@ -1210,9 +1200,9 @@ client.on(client.Event.SDK_UPDATE, () => { console.log('The SDK has been updated!'); }); -// This event fires only using the LocalStorage option and if there's Split data stored in the browser. +// This event fires only using the LocalStorage option and if there's FME data stored in the browser. client.once(client.Event.SDK_READY_FROM_CACHE, function () { - // Fired after the SDK could confirm the presence of the Split data. + // Fired after the SDK could confirm the presence of the FME data. // This event fires really quickly, since there's no actual fetching of information. // Keep in mind that data might be stale, this is NOT a replacement of SDK_READY. }); @@ -1222,31 +1212,31 @@ client.once(client.Event.SDK_READY_FROM_CACHE, function () { ### Sharing state with a pluggable storage -By default, the SDK fetches the feature flags and segments it needs to compute treatments from Split cloud, and stores it in its cache. As a result, this makes it easy to get set up with Split by instantiating the SDK, waiting for the `SDK_READY` event, and starting to use it. This default execution mode, called standalone mode, is appropriate for mobile and Web apps running on the client-side. However, in a stateless environment, like many serverless or edge computing solutions, this model could lead to some performance implications. +By default, the SDK fetches the feature flags and segments it needs to compute treatments from Harness FME servers, and stores it in its cache. As a result, this makes it easy to get set up with Harness FME by instantiating the SDK, waiting for the `SDK_READY` event, and starting to use it. This default execution mode, called standalone mode, is appropriate for mobile and Web apps running on the client-side. However, in a stateless environment, like many serverless or edge computing solutions, this model could lead to some performance implications. Unlike a Web app or a traditional server process, code running in a stateless environment generally has a lifecycle that is much shorter because it is associated to the lifetime of a single HTTP request/response cycle. Therefore, for each incoming HTTP request, the code must instantiate the SDK and wait until it fetches its state before evaluating, which impacts the response latency. Also, the pricing model of serverless providers usually depends on the average duration of your code and the outgoing HTTP requests. -To optimize latency, externalize the state of the SDK in a data storage available on the same infrastructure where SDKs are instantiated and instruct the SDKs to "consume" data from that storage instead of fetching it from Split cloud. This is known as consumer mode, which has two variants, *consumer* and *partial consumer*, as illustrated in the following diagram. +To optimize latency, externalize the state of the SDK in a data storage available on the same infrastructure where SDKs are instantiated and instruct the SDKs to "consume" data from that storage instead of fetching it from Harness FME servers. This is known as consumer mode, which has two variants, *consumer* and *partial consumer*, as illustrated in the following diagram.

sdk_modes.png

-#### Synchronizing Split data on your storage +#### Synchronizing FME data on your storage -As illustrated in the previous diagram, running the SDK in consumer mode requires an additional component for synchronizing the Split data in your storage, which is known as [Synchronizer](https://help.split.io/hc/en-us/articles/4421513571469). +As illustrated in the previous diagram, running the SDK in consumer mode requires an additional component for synchronizing the FME data in your storage, which is known as [Synchronizer](https://help.split.io/hc/en-us/articles/4421513571469). #### Consumer modes In *consumer* and *partial consumer* modes, the SDK evaluates treatments by retrieving rollout plans from a shared data storage. The difference between each mode is in how generated impressions and events are handled: -* In consumer mode, the SDK uses the shared storage to store impressions and events, instead of submitting them directly to Split cloud. The synchronizer is in charge of submitting this data to Split. -* In partial consumer mode, the SDK behaves as in standalone mode, submitting events and impressions to Split cloud. In this mode, [configuration parameters](#configuration) that affects how events and impressions are submitted, such as changing a push rate or the events queue size, change the behavior of the SDK. +* In consumer mode, the SDK uses the shared storage to store impressions and events, instead of submitting them directly to Harness FME servers. The synchronizer is in charge of submitting this data to Harness FME. +* In partial consumer mode, the SDK behaves as in standalone mode, submitting events and impressions to Harness FME servers. In this mode, [configuration parameters](#configuration) that affects how events and impressions are submitted, such as changing a push rate or the events queue size, change the behavior of the SDK. To instantiate an SDK working as consumer, set two configs on the root of the configuration object, `mode` and `storage`. Set `mode` with the mode of choice, either `'consumer'` or `'consumer_partial'`. Then set `storage` to a valid **storage wrapper** which is the adapter used to connect to the data storage. -The following shows how to configure and get treatments for a Split SDK instance in consumer or partial consumer mode: +The following shows how to configure and get treatments for a SDK instance in consumer or partial consumer mode: @@ -1301,15 +1291,15 @@ client.once(client.Event.SDK_READY_TIMED_OUT, function () { -You can write your own custom storage wrapper for the Split client by extending the IPluggableStorageWrapper interface. +You can write your own custom storage wrapper for the SDK factory client by extending the IPluggableStorageWrapper interface. #### Storage wrapper examples We currently maintain storage wrappers for the following technologies: -- **Cloudflare Durable Objects**: The Durable Object wrapper is available as part of a [template](https://github.com/splitio/cloudflare-workers-template) to help you kick-start a [Cloudflare Workers](https://developers.cloudflare.com/workers/) project. In addition to providing the wrapper, the template demonstrates the basic setup of the Split SDK and Synchronizer. +- **Cloudflare Durable Objects**: The Durable Object wrapper is available as part of a [template](https://github.com/splitio/cloudflare-workers-template) to help you kick-start a [Cloudflare Workers](https://developers.cloudflare.com/workers/) project. In addition to providing the wrapper, the template demonstrates the basic setup of the SDK and Synchronizer. -- **Vercel Edge Config**: The wrapper is available as an [NPM package](https://npmjs.com/package/@splitsoftware/vercel-integration-utils) and wraps the [Edge Config](https://vercel.com/docs/storage/edge-config) data store. This wrapper is used with the [Split Integration for Vercel](https://vercel.com/integrations/split). Adding the Split integration to a Vercel project sets up the synchronization of the Edge Config data store. See the [Vercel Split integration guide](https://help.split.io/hc/en-us/articles/16469873148173) for more information about the integration setup. +- **Vercel Edge Config**: The wrapper is available as an [NPM package](https://npmjs.com/package/@splitsoftware/vercel-integration-utils) and wraps the [Edge Config](https://vercel.com/docs/storage/edge-config) data store. This wrapper is used with the [FME Integration for Vercel](https://vercel.com/integrations/split). Adding the FME integration to a Vercel project sets up the synchronization of the Edge Config data store. See the [Vercel FME integration guide](https://help.split.io/hc/en-us/articles/16469873148173) for more information about the integration setup. ### User consent @@ -1318,8 +1308,8 @@ The SDK allows you to disable the tracking of events and impressions until user The `userConsent` configuration parameter lets you set the initial consent status of the SDK instance, and the factory method `UserConsent.setStatus(boolean)` lets you grant (enable) or decline (disable) dynamic data tracking. There are three possible initial states: - * `'GRANTED'`: The user grants consent for tracking events and impressions. The SDK sends them to Split cloud. This is the default value if `userConsent` param is not defined. - * `'DECLINED'`: The user declines consent for tracking events and impressions. The SDK does not send them to Split cloud. + * `'GRANTED'`: The user grants consent for tracking events and impressions. The SDK sends them to Harness FME servers. This is the default value if `userConsent` param is not defined. + * `'DECLINED'`: The user declines consent for tracking events and impressions. The SDK does not send them to Harness FME servers. * `'UNKNOWN'`: The user neither grants nor declines consent for tracking events and impressions. The SDK tracks them in its internal storage, and eventually either sends them or not if the consent status is updated to `'GRANTED'` or `'DECLINED'` respectively. The status can be updated at any time with the `UserConsent.setStatus` factory method. @@ -1334,7 +1324,7 @@ var factory = SplitFactory({ }, // Overwrites the initial consent status of the factory instance, which is 'GRANTED' by default. // 'UNKNOWN' status represents that the user has neither granted nor declined consent for tracking data, - // so the SDK will locally track data but not send it to Split cloud until consent is changed to 'GRANTED'. + // so the SDK will locally track data but not send it to Harness FME servers until consent is changed to 'GRANTED'. userConsent: 'UNKNOWN' }); @@ -1343,15 +1333,15 @@ factory.UserConsent.getStatus() === factory.UserConsent.Status.UNKNOWN; // `setStatus` method lets you update the factory consent status at any time. // Pass `true` for 'GRANTED' and `false` for 'DECLINED'. -factory.UserConsent.setStatus(true); // Consent status changed from 'UNKNOWN' to 'GRANTED'. Data will be sent to Split cloud. +factory.UserConsent.setStatus(true); // Consent status changed from 'UNKNOWN' to 'GRANTED'. Data will be sent to Harness FME servers. factory.UserConsent.getStatus() === factory.UserConsent.Status.GRANTED; -factory.UserConsent.setStatus(false); // Consent status changed from 'GRANTED' to 'DECLINED'. Data will not be sent to Split cloud. +factory.UserConsent.setStatus(false); // Consent status changed from 'GRANTED' to 'DECLINED'. Data will not be sent to Harness FME servers. factory.UserConsent.getStatus() === factory.UserConsent.Status.DECLINED; ``` ## Example apps -The following are example applications detailing how to configure and instantiate the Split JavaScript Browser SDK on commonly used platforms. +The following are example applications detailing how to configure and instantiate the JavaScript Browser SDK on commonly used platforms. * [Basic HTML](https://github.com/splitio/example-javascript-client) diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/flutter-plugin.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/flutter-plugin.md index e1eb5950834..69f49cfc142 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/flutter-plugin.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/flutter-plugin.md @@ -26,7 +26,7 @@ This plugin currently supports the Android and iOS platforms. ## Initialization -Set up Split in your code base with the following two steps: +Set up FME in your code base with the following two steps: ### 1. Add the package in your pubspec.yaml file @@ -49,17 +49,17 @@ final Splitio _split = Splitio('YOUR_SDK_KEY', 'KEY'); We recommend instantiating the `Splitio` object once as a singleton and reusing it throughout your application. -Configure the plugin with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split UI, on your Admin settings page, API keys section. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. +Configure the plugin with the SDK key for the FME environment that you would like to access. The SDK key is available in Harness FME Admin settings. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. ## Using the plugin ### Basic use -When the SDK is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds, depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while it's in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). +When the SDK is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Harness servers. This process can take up to a few hundred milliseconds, depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while it's in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). To make sure the SDK is properly loaded before asking it for a treatment, wait until the SDK is ready, as shown below. You can use the `onReady` parameter when creating the client to get notified when this happens. -After the observable calls back, you can use the `getTreatment` method to return the proper treatment based on the `FEATURE_FLAG_NAME` and the `key` variable you passed when instantiating the SDK. Then use an if-else-if block as shown below and insert the code for the different treatments that you defined in the Split user interface. Remember the final else branch in your code to handle the client returning control. +After the observable calls back, you can use the `getTreatment` method to return the proper treatment based on the `FEATURE_FLAG_NAME` and the `key` variable you passed when instantiating the SDK. Then use an if-else-if block as shown below and insert the code for the different treatments that you defined in Harness FME. Remember the final else branch in your code to handle the client returning control. ```dart title="Flutter" /// Get treatment @@ -80,7 +80,7 @@ _split.client(onReady: (client) async { To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), pass the client's `getTreatment` method as an attribute map at runtime. -In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in the Split user interface to decide whether to show the `on` or `off` treatment to this account. The `getTreatment` method supports five types of attributes: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in Harness FME to decide whether to show the `on` or `off` treatment to this account. The `getTreatment` method supports five types of attributes: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: * **Strings:** Use type String. * **Numbers:** Use type num (int or double). @@ -157,7 +157,7 @@ var result = await client.clearAttributes(); ### Multiple evaluations at once -In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` from the Split client to do this. +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` from the SDK factory client to do this. * `getTreatments`: Pass a list of the feature flag names you want treatments for. * `getTreatmentsByFlagSet`: Evaluate all flags that are part of the provided set name and are cached on the SDK instance. * `getTreatmentsByFlagSets`: Evaluate all flags that are part of the provided set names and are cached on the SDK instance. @@ -206,7 +206,7 @@ class SplitResult { } ``` -The config element is a stringified version of the configuration JSON defined in the Split user interface. If there is no configuration defined for a treatment, the SDK returns `null` for the config parameter. +The config element is a stringified version of the configuration JSON defined in Harness FME. If there is no configuration defined for a treatment, the SDK returns `null` for the config parameter. This method taskes the exact set of arguments as the standard `getTreatment` method. See below examples on proper usage: @@ -276,7 +276,7 @@ if (treatment == 'on') { ### Shutdown -Call the `client.destroy()` method once you've stopped using the client, as this method gracefully shuts down the Split SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. +Call the `client.destroy()` method once you've stopped using the client, as this method gracefully shuts down the SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. ```dart title="Flutter" /// You should call destroy() on the client once it is no longer needed: @@ -287,7 +287,7 @@ After `destroy()` is called and finishes, any subsequent invocations to `getTrea ## Track -Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users' actions and metrics. [Learn more about using track events](https://help.split.io/hc/en-us/articles/360020585772) in Split. +Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Harness FME and allows you to measure the impact of your feature flags on your users' actions and metrics. [Learn more about using track events](https://help.split.io/hc/en-us/articles/360020585772). In the examples below, you can see that the `.track()` method can take up to four arguments. The proper data type and syntax for each are: @@ -296,11 +296,11 @@ In the examples below, you can see that the `.track()` method can take up to fou * Starts with a letter or number. * Contains only letters, numbers, hyphen, underscore, or period. * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` -* **TRAFFIC_TYPE:** (Optional) The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **TRAFFIC_TYPE:** (Optional) The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined Harness FME. * **VALUE:** (Optional) The value is used to create the metric. The expected data type is **double**. -* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about [event properties](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties). Split currently supports three types of properties: strings, numbers, and booleans. +* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about [event properties](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties). FME currently supports three types of properties: strings, numbers, and booleans. -The `track` method returns a boolean value of `true` or `false` to indicate whether or not the client was able to successfully queue the event to be sent back to Split's servers on the next event post. The service returns `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method is provided. +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the client was able to successfully queue the event to be sent back to Harness servers on the next event post. The service returns `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method is provided. In the case that a bad input is provided, you can read more about our [SDK's expected behavior](https://help.split.io/hc/en-us/articles/360020585772-Track-events). @@ -333,12 +333,12 @@ The parameters available for configuration are shown below. | **Configuration** | **Description** | **Default value** | | --- | --- | --- | -| featuresRefreshRate | The SDK polls Split servers for changes to feature flags at this rate (in seconds). | 3600 seconds | -| segmentsRefreshRate | The SDK polls Split servers for changes to segments at this rate (in seconds). | 1800 seconds | -| impressionsRefreshRate | The treatment log captures which customer saw what treatment (on, off, etc.) at what time. This log is periodically flushed back to Split servers. This configuration controls how quickly the cache expires after a write (in seconds). | 1800 seconds | -| telemetryRefreshRate | The SDK caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers (in seconds). | 3600 seconds (1 hour) | +| featuresRefreshRate | The SDK polls Harness servers for changes to feature flags at this rate (in seconds). | 3600 seconds | +| segmentsRefreshRate | The SDK polls Harness servers for changes to segments at this rate (in seconds). | 1800 seconds | +| impressionsRefreshRate | The treatment log captures which customer saw what treatment (on, off, etc.) at what time. This log is periodically flushed back to Harness servers. This configuration controls how quickly the cache expires after a write (in seconds). | 1800 seconds | +| telemetryRefreshRate | The SDK caches diagnostic data that it periodically sends to Harness servers. This configuration controls how frequently this data is sent back to Harness servers (in seconds). | 3600 seconds (1 hour) | | eventsQueueSize | When using `.track`, the number of **events** to be kept in memory. | 10000 | -| eventFlushInterval | When using `.track`, how often is the events queue flushed to Split's servers. | 1800 seconds | +| eventFlushInterval | When using `.track`, how often is the events queue flushed to Harness servers. | 1800 seconds | | eventsPerPush | Maximum size of the batch to push events. | 2000 | | trafficType | When using `.track`, the default traffic type to be used. | not set | | impressionsQueueSize | Default queue size for impressions. | 30K | @@ -347,17 +347,17 @@ The parameters available for configuration are shown below. | persistentAttributesEnabled | Enables saving attributes on persistent cache which is loaded as part of the SDK_READY_FROM_CACHE flow. All functions that mutate the stored attributes map affect the persistent cache. | false | | impressionListener | Enables impression listener. If true, generated impressions stream in the impressionsStream() method of Splitio. | false | | syncConfig | Use it to filter specific feature flags to be synced and evaluated by the SDK. It can be created with the `SyncConfig.flagSets('sets')` method (recommended, flag sets aree available in all tiers) or `SyncConfig(names: ["feature-flag-1", "feature-flag-2"])` for individual names. If not set, all flags are downloaded. | not set | -| syncEnabled | Controls the SDK continuous synchronization flags. When `true`, a running SDK processes the rollout plan updates which is performed in the Split user interface (default). When `false`, it fetches all data upon init, which ensures a consistent experience during a user session and optimizes resources when these updates are not consumed by the app. | true | +| syncEnabled | Controls the SDK continuous synchronization flags. When `true`, a running SDK processes the rollout plan updates which is performed in Harness FME (default). When `false`, it fetches all data upon init, which ensures a consistent experience during a user session and optimizes resources when these updates are not consumed by the app. | true | | userConsent | User consent status controls the tracking of events and impressions. Possible values are `UserConsent.granted`, `UserConsent.decline`, and `UserConsent.unknown`. See [User consent](#user-consent) for details. | `UserConsent.granted` | | encryptionEnabled | Enables or disables encryption for cached data. | `false` | | logLevel | Enables logging according to the level specified. Options are `SplitLogLevel.none`, `SplitLogLevel.verbose`, `SplitLogLevel.debug`, `SplitLogLevel.info`, `SplitLogLevel.warning`, and `SplitLogLevel.error`. | `SplitLogLevel.none` | -| impressionsMode | This configuration defines how impressions (decisioning events) are queued. Supported modes are `ImpressionsMode.optimized`, `ImpressionsMode.none`, and `ImpressionsMode.debug`. In `ImpressionsMode.optimized` mode, only unique impressions are queued and posted to Split; this is the recommended mode for experimentation use cases. In `ImpressionsMode.none` mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use `ImpressionsMode.none` when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In `ImpressionsMode.debug` mode, ALL impressions are queued and sent to Split. This is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. | `ImpressionsMode.optimized` | +| impressionsMode | This configuration defines how impressions (decisioning events) are queued. Supported modes are `ImpressionsMode.optimized`, `ImpressionsMode.none`, and `ImpressionsMode.debug`. In `ImpressionsMode.optimized` mode, only unique impressions are queued and posted to Harness; this is the recommended mode for experimentation use cases. In `ImpressionsMode.none` mode, no impression is tracked in Harness FME and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use `ImpressionsMode.none` when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In `ImpressionsMode.debug` mode, ALL impressions are queued and sent to Harness. This is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. | `ImpressionsMode.optimized` | | readyTimeout | Maximum amount of time (in seconds) to wait until the `onTimeout` callback is fired or `whenTimeout` future is completed. A negative value means no timeout. | 10 seconds | | certificatePinningConfiguration | If set, enables certificate pinning for the given domains. For details, see the [Certificate pinning](#certificate-pinning) section below. | null | ## Manager -Use these methods on Splitio instance to get a list of the feature flags available to the Split client. +Use these methods on Splitio instance to get a list of the feature flags available to the SDK factory client. ```dart title="Flutter" /// Retrieves the feature flags that are currently registered with the SDK. @@ -387,7 +387,7 @@ class SplitView { ## Listener -Split SDKs send impression data back to Split servers periodically as a result of evaluating feature flags. To additionally send this information to a location of your choice, use the `impressionsStream`. +FME SDKs send impression data back to Harness servers periodically as a result of evaluating feature flags. To additionally send this information to a location of your choice, use the `impressionsStream`. This provides a stream that publishes `Impression` objects every time one is generated. @@ -436,9 +436,9 @@ This section describes advanced use cases and features provided by the SDK. ### Instantiate multiple SDK clients -Split supports the ability to release based on multiple traffic types. For example, with traffic types, you can release to `users` in one feature flag and `accounts` in another. If you are unfamiliar with using multiple traffic types, refer to the [Traffic type guide](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) for more information. +FME supports the ability to release based on multiple traffic types. For example, with traffic types, you can release to `users` in one feature flag and `accounts` in another. If you are unfamiliar with using multiple traffic types, refer to the [Traffic type guide](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) for more information. -Each SDK client is tied to one specific customer ID at a time, so if you need to roll out feature flags by different keys, instantiate multiple SDK clients, one for each traffic type. For example, you may want to roll out the feature `USER_POLL` by `users` and the feature `ACCOUNT_PERMISSIONING` by `accounts`. You can do this with the example below: +Each SDK factory client is tied to one specific customer ID at a time, so if you need to roll out feature flags by different keys, instantiate multiple SDK clients, one for each traffic type. For example, you may want to roll out the feature `USER_POLL` by `users` and the feature `ACCOUNT_PERMISSIONING` by `accounts`. You can do this with the example below: ```dart title="Flutter" final Splitio _split = Splitio('YOUR_SDK_KEY', 'ACCOUNT_ID'); @@ -474,9 +474,9 @@ While the SDK does not put any limitations on the number of instances that can b You can subscribe to four different callbacks when creating a client. * `onReadyFromCache`. This event fires once the SDK is ready to evaluate treatments using a locally cached version of your rollout plan from a previous session (which might be stale). If there is data in the cache, this event fires almost immediately, since access to the cache is fast; otherwise, it doesn't fire. -* `onReady`. This event fires once the SDK is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Split servers. -* `onTimeout`. This event fires if there is no cached version of your rollout plan in disk cache, and the SDK could not fully download the data from Split servers within the time specified by the `readyTimeout` configuration parameter. This event does not indicate that the SDK initialization was interrupted. The SDK continues downloading the rollout plan and fires the `onReady` event when finished. This delayed `onReady` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. -* `onUpdate`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in the Split user interface. +* `onReady`. This event fires once the SDK is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Harness servers. +* `onTimeout`. This event fires if there is no cached version of your rollout plan in disk cache, and the SDK could not fully download the data from Harness servers within the time specified by the `readyTimeout` configuration parameter. This event does not indicate that the SDK initialization was interrupted. The SDK continues downloading the rollout plan and fires the `onReady` event when finished. This delayed `onReady` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. +* `onUpdate`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in Harness FME. ```dart title="Flutter" final Splitio _split = Splitio('YOUR_SDK_KEY', 'ACCOUNT_ID'); @@ -484,7 +484,7 @@ final Splitio _split = Splitio('YOUR_SDK_KEY', 'ACCOUNT_ID'); _split.client(onReady: (client) { /// Client has fetched the most up-to-date definitions. }, onReadyFromCache: (client) { - /// Fired after the SDK could confirm the presence of the Split data. + /// Fired after the SDK could confirm the presence of the FME data. /// This event fires really quickly, since there's no actual fetching of information. /// Keep in mind that data might be stale, this is NOT a replacement of sdkReady. }, onUpdated: (client) { @@ -504,7 +504,7 @@ _client.whenReady().then((client) { }); _client.whenReadyFromCache((client) { - /// Fired after the SDK could confirm the presence of the Split data. + /// Fired after the SDK could confirm the presence of the FME data. /// This event fires really quickly, since there's no actual fetching of information. /// Keep in mind that data might be stale, this is NOT a replacement of sdkReady. }); @@ -528,23 +528,23 @@ The `userConsent` configuration parameter lets you set the initial consent statu The following are the three possible initial states: - * `UserConsent.granted`. The user grants consent for tracking events and impressions. The SDK sends them to the Split cloud. This is the default value if the `userConsent` param is not defined. - * `UserConsent.declined`. The user declines consent for tracking events and impressions. The SDK does not send them to the Split cloud. + * `UserConsent.granted`. The user grants consent for tracking events and impressions. The SDK sends them to the Harness FME servers. This is the default value if the `userConsent` param is not defined. + * `UserConsent.declined`. The user declines consent for tracking events and impressions. The SDK does not send them to the Harness FME servers. * `UserConsent.unknown`. The user neither grants nor declines consent for tracking events and impressions. The SDK tracks them in its internal storage, and eventually either sends them or not if the consent status is updated to `'GRANTED'` or `'DECLINED'` respectively. The status can be updated at any time with the `setUserConsent` factory method. ```dart title="User consent: initial config, getter and setter" // Overwrites the initial consent status of the factory instance, which is 'GRANTED' by default. // 'UNKNOWN' status represents that the user has neither granted nor declined consent for tracking data, - // so the SDK locally tracks data but not send it to Split cloud until consent is changed to 'GRANTED'. + // so the SDK locally tracks data but not send it to Harness FME servers until consent is changed to 'GRANTED'. final Splitio _split = Splitio(_sdkKey, _matchingKey, configuration: SplitConfiguration( userConsent: UserConsent.unknown, )); - // Changed User Consent status to 'GRANTED'. Data will be sent to Split cloud. + // Changed User Consent status to 'GRANTED'. Data will be sent to Harness FME servers. _split.setUserConsent(true); - // Changed User Consent status to 'DECLINED'. Data will not be sent to Split cloud. + // Changed User Consent status to 'DECLINED'. Data will not be sent to Harness FME servers. _split.setUserConsent(false); // The 'getUserConsent' method returns User Consent status. @@ -579,7 +579,7 @@ To set the plugin to require pinned certificates for specific hosts, add the `Ce // Provide a base 64 SHA-256 hash .addPin("*.example1.com", "sha256/7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y="); - // Set the CertificatePinningConfiguration property for the Split client configuration + // Set the CertificatePinningConfiguration property for the SDK factory client configuration SplitConfiguration config = SplitConfiguration( certificatePinningConfiguration: pinningConfig); @@ -587,7 +587,7 @@ To set the plugin to require pinned certificates for specific hosts, add the `Ce ### Link with native factory -A native Split Factory instance can be shared with the plugin to save resources when evaluations need to be performed on native platform logic. To do so, do the following: +A native SplitFactory instance can be shared with the plugin to save resources when evaluations need to be performed on native platform logic. To do so, do the following: #### Android @@ -606,7 +606,7 @@ public class CustomApplication extends Application { android:icon="@mipmap/ic_launcher"> ``` -2. Add the Split Android SDK dependency to your project's `build.gradle` file. +2. Add the Android SDK dependency to your project's `build.gradle` file. ```groovy title="Gradle" dependencies { @@ -659,7 +659,7 @@ public class CustomApplication extends Application implements SplitFactoryProvid #### iOS -1. Add the Split iOS SDK dependency to your app's `Podfile`. +1. Add the iOS SDK dependency to your app's `Podfile`. ```podfile title="Podfile" pod 'Split', '~> 2.15.0' diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/ios-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/ios-sdk.md index aa8241101b0..3daa41eb5e3 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/ios-sdk.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/ios-sdk.md @@ -20,7 +20,7 @@ This library is compatible with iOS and tvOS deployment target versions 9.0+, ma ## Initialization -To get started, set up Split in your code base with the two following steps. +To get started, set up FME in your code base with the two following steps. ### 1. Import the SDK into your project @@ -46,22 +46,22 @@ github "splitio/ios-client" 3.1.1 Once added, follow the steps provided in the [Carthage Readme](https://github.com/Carthage/Carthage/blob/master/README.md#if-youre-building-for-ios-tvos-or-watchos). -### 2. Instantiate the SDK and create a new Split client +### 2. Instantiate the SDK and create a new SDK factory client -The first time that the SDK is instantiated, it starts background tasks to update an in-memory cache and in-storage cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds depending on the size of the data. +The first time that the SDK is instantiated, it starts background tasks to update an in-memory cache and in-storage cache with small amounts of data fetched from Harness servers. This process can take up to a few hundred milliseconds depending on the size of the data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while it is in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). After the first initialization, the fetched data is stored. Further initializations fetch data from that cache and the configuration is available immediately. -We recommend instantiating the Split factory once as a singleton and reusing it throughout your application. +We recommend instantiating the SDK factory once as a singleton and reusing it throughout your application. -Configure the SDK with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split UI, on your Admin settings page, API keys section. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. +Configure the SDK with the SDK key for the FME environment that you would like to access. The SDK key is available in Harness FME Admin settings. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. ```swift title="Swift" import Split -// Your Split SDK Key +// Your SDK key let sdkKey: String = "YOUR_SDK_KEY" //User Key @@ -70,14 +70,14 @@ let sdkKey: String = "YOUR_SDK_KEY" // This could also be a UUID you generate for anonymous users. let key: Key = Key(matchingKey: "key") -//Split Configuration +// Configuration let config = SplitClientConfig() -//Split Factory +//SDK Factory let builder = DefaultSplitFactoryBuilder() let factory = builder.setApiKey(sdkKey).setKey(key).setConfig(config).build() -//Split Client +//SDK Factory Client let client = factory?.client ``` @@ -89,11 +89,11 @@ To make sure the SDK is properly loaded before asking it for a treatment, wait u Once the `sdkReady` event fires, you can use the `getTreatment` method to return the proper treatment based on the feature flag name you pass and the key you passed when instantiating the SDK. -From there, you need to use an if-else-if block as shown below and plug the code in for the different treatments that you defined in the Split user interface. Make sure to remember the final else branch in your code to handle the client returning control. +From there, you need to use an if-else-if block as shown below and plug the code in for the different treatments that you defined in Harness FME. Make sure to remember the final else branch in your code to handle the client returning control. ```swift title="Swift" client?.on(event: SplitEvent.sdkReady) { - // Evaluate feature flag in Split + // Evaluate feature flag let treatment = client?.getTreatment("FEATURE_FLAG_NAME") if treatment == "on" { @@ -114,7 +114,7 @@ Also, a `sdkReadyFromCache` event is available, which allows you to be aware of ```swift title="Swift" client?.on(event: SplitEvent.sdkReadyFromCache) { - // Evaluate feature flag in Split + // Evaluate feature flag let treatment = client?.getTreatment("FEATURE_FLAG_NAME") if treatment == "on" { @@ -152,7 +152,7 @@ client?.on(event: SplitEvent.sdkReadyFromCache, queue: customQueue) { To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK's `getTreatment` method needs to be passed an attribute map at runtime. -In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the Rollout plan as defined in the Split user interface to decide whether to show the `on` or `off` treatment to this account. +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the Rollout plan as defined in Harness FME to decide whether to show the `on` or `off` treatment to this account. The `getTreatment` method supports five types of attributes: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: @@ -267,7 +267,7 @@ let result = client.clearAttributes() ### Multiple evaluations at once -In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` method of the Split client to do this. +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` method of the SDK factory client to do this. * `getTreatments`: Pass a list of the feature flag names you want treatments for. * `getTreatmentsByFlagSet`: Evaluate all flags that are part of the provided set name and are cached on the SDK instance. * `getTreatmentsByFlagSets`: Evaluate all flags that are part of the provided set names and are cached on the SDK instance. @@ -299,7 +299,7 @@ let treatmentsByFlagSets = client.getTreatmentsByFlagSets(flagSets, attributes: To [leverage dynamic configurations with your treatments](https://help.split.io/hc/en-us/articles/360026943552), use the `getTreatmentWithConfig` methods. These methods returns an object containing the treatment and associated configuration. -The config element is a stringified version of the configuration JSON defined in the Split user interface. If there is no configuration defined for a treatment, the SDK returns `null` for the config parameter. +The config element is a stringified version of the configuration JSON defined in Harness FME. If there is no configuration defined for a treatment, the SDK returns `null` for the config parameter. This method takes the exact same set of arguments as the standard `getTreatment` method. See below for examples on proper usage: @@ -329,7 +329,7 @@ let treatmentsByFlagSets = client.getTreatmentsWithConfigByFlagSets(flagSets, at ### Shutdown -Before letting your app shut down, call `destroy()` as it gracefully shuts down the Split SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions and events. +Before letting your app shut down, call `destroy()` as it gracefully shuts down the SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions and events. ```swift title="Swift" client?.destroy() @@ -353,22 +353,22 @@ A call to the `destroy()` method also destroys the factory object. When creating ## Track -Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users’ actions and metrics. +Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Harness FME and allows you to measure the impact of your feature flags on your users’ actions and metrics. [Learn more](https://help.split.io/hc/en-us/articles/360020585772) about using track events in feature flags. In the examples below, you can see that the `.track()` method can take up to four arguments. The proper data type and syntax for each are: -* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined Harness FME. * **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: * Contains 63 characters or fewer. * Starts with a letter or number. * Contains only letters, numbers, hyphen, underscore, or period. * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}`. * **VALUE:** (Optional) The value used in creating the metric. This field can be sent in as null or 0 if you intend to only use the count function when creating a metric. The expected data type is **Double**. -* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. +* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. FME currently supports three types of properties: strings, numbers, and booleans. -The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event sent back to Split's servers on the next event post. The SDK returns `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method is provided. +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event sent back to Harness servers on the next event post. The SDK returns `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method is provided. In case a bad input is provided, you can read more about our SDK's expected behavior in our [Events](https://help.split.io/hc/en-us/articles/360020585772-Track-events) guide. @@ -408,7 +408,7 @@ let resp = client?.track(eventType: "page_load_time", proerties: properties) ## Manager -Use the Split Manager to get a list of feature flags available to the Split client. +Use the Split Manager to get a list of feature flags available to the SDK factory client. To instantiate a Manager in your code base, use the same factory that you used for your client. @@ -479,16 +479,16 @@ The SDK has a number of knobs for configuring performance. Each knob is tuned to | **Configuration** | **Description** | **Default value** | | --- | --- | --- | -| featuresRefreshRate | The SDK polls Split servers for changes to feature flags at this rate (in seconds). | 3600 seconds (1 hour) | -| segmentsRefreshRate | The SDK polls Split servers for changes to segments at this rate (in seconds). | 1800 seconds (30 minutes) | -| impressionRefreshRate | The treatment log captures which customer saw what treatment (on, off, etc.) at what time. This log is periodically flushed back to Split servers. This configuration controls how quickly the cache expires after a write (in seconds). | 1800 seconds (30 minutes) | +| featuresRefreshRate | The SDK polls Harness servers for changes to feature flags at this rate (in seconds). | 3600 seconds (1 hour) | +| segmentsRefreshRate | The SDK polls Harness servers for changes to segments at this rate (in seconds). | 1800 seconds (30 minutes) | +| impressionRefreshRate | The treatment log captures which customer saw what treatment (on, off, etc.) at what time. This log is periodically flushed back to Harness servers. This configuration controls how quickly the cache expires after a write (in seconds). | 1800 seconds (30 minutes) | | impressionsQueueSize | Default queue size for impressions. | 30K | -| eventsPushRate | When using `.track`, how often the events queue is flushed to Split servers. | 1800 seconds| +| eventsPushRate | When using `.track`, how often the events queue is flushed to Harness servers. | 1800 seconds| | eventsPerPush | Maximum size of the batch to push events. | 2000 | | eventsFirstPushWindow | Amount of time to wait for the first flush. | 10 seconds | | eventsQueueSize | When using `.track`, the number of **events** to be kept in memory. | 10000 | | trafficType | (optional) The default traffic type for events tracked using the `track` method. If not specified, every `track` call should specify a traffic type. | not set | -| telemetryRefreshRate | The SDK caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers (in seconds). | 3600 seconds (1 hour) | +| telemetryRefreshRate | The SDK caches diagnostic data that it periodically sends to Harness servers. This configuration controls how frequently this data is sent back to Harness servers (in seconds). | 3600 seconds (1 hour) | | logLevel | Enables logging according to the level specified. Options are `NONE`, `VERBOSE`, `DEBUG`, `INFO`, `WARNING`, and `ERROR`. | `NONE` | | synchronizeInBackground | Activates synchronization when application host is in background. | false | | streamingEnabled | Boolean flag to enable the streaming service as default synchronization mechanism when in foreground. In the event of an issue with streaming, the SDK falls back to the polling mechanism. If false, the SDK polls for changes as usual without attempting to use streaming. | true | @@ -496,8 +496,8 @@ The SDK has a number of knobs for configuring performance. Each knob is tuned to | offlineRefreshRate | The SDK periodically reloads the localhost mocked feature flags at this given rate in seconds. This can be turned off by setting it to -1 instead of a positive number. | -1 (off) | | sdkReadyTimeOut | Amount of time in milliseconds to wait before notifying a timeout. | -1 (not set) | | persistentAttributesEnabled | Enables saving attributes on persistent cache which is loaded as part of the SDK_READY_FROM_CACHE flow. All functions that mutate the stored attributes map affect the persistent cache.| false | -| syncEnabled | Controls the SDK continuous synchronization flags. When `true`, a running SDK processes rollout plan updates performed in the Split user interface (default). When `false`, it fetches all data upon init, which ensures a consistent experience during a user session and optimizes resources when these updates are not consumed by the app. | true | -| impressionsMode | This configuration defines how impressions (decisioning events) are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Split; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, ALL impressions are queued and sent to Split; this is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. | `OPTIMIZED` | +| syncEnabled | Controls the SDK continuous synchronization flags. When `true`, a running SDK processes rollout plan updates performed in Harness FME (default). When `false`, it fetches all data upon init, which ensures a consistent experience during a user session and optimizes resources when these updates are not consumed by the app. | true | +| impressionsMode | This configuration defines how impressions (decisioning events) are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Harness; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Harness FME and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, ALL impressions are queued and sent to Harness; this is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. | `OPTIMIZED` | | userConsent | User consent status used to control the tracking of events and impressions. Possible values are `GRANTED`, `DECLINED`, and `UNKNOWN`. See the [User consent](#user-consent) section for details. | `GRANTED` | | encryptionEnabled | Enables or disables encryption for cached data. | `false` | | httpsAuthenticator | If set, the SDK uses it to authenticate network requests. To set this value, an implementation of SplitHttpAuthenticator must be provided. | `nil` | @@ -509,13 +509,13 @@ To set each of the parameters defined above, use the syntax below: ```swift title="Swift" import Split -// Your Split SDK key +// Your SDK key let sdkKey: String = "YOUR_SDK_KEY" //User Key let key: Key = Key(matchingKey: "key") -//Split Configuration +//Configuration let config = SplitClientConfig() config.impressionRefreshRate = 30 config.isDebugModeEnabled = false @@ -524,18 +524,18 @@ let syncConfig = SyncConfig.builder() .build() config.sync = syncConfig -//Split Factory +//SDK Factory let builder = DefaultSplitFactoryBuilder() let factory = builder.setApiKey(sdkKey).setKey(key).setConfig(config).build() -//Split Client +//SDK Client let client = factory?.client ``` ## Localhost mode -For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the Split SDK can be started in 'localhost' mode. In this mode, the SDK neither polls nor updates Split servers, rather it uses an in-memory data structure to determine what treatments to show to the customer for each of the features. +For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the SDK can be started in 'localhost' mode. In this mode, the SDK neither polls nor updates Harness servers, rather it uses an in-memory data structure to determine what treatments to show to the customer for each of the features. To use the SDK in localhost mode, replace the API Key with "localhost", as shown in the example below: @@ -564,10 +564,10 @@ In the example above, we have four entries: * The third entry defines that `my_feature` always returns `off` for all keys that don't match another entry. In this case, any key other than `key`. * The fourth entry shows how an example to override a treatment for a set of keys. -You can set the name of the Split localhost YAML file within cache folder as shown in the example below: +You can set the name of the localhost YAML file within cache folder as shown in the example below: ```swift title="Swift" -// Split SDK key must be "localhost" +// SDK key must be "localhost" let apiKey: String = "localhost" let key: Key = Key(matchingKey: "key") let config = SplitClientConfig() @@ -577,12 +577,12 @@ self.factory = builder.setApiKey("localhost").setKey(key).setConfig(config).build() ``` -If SplitClientConfig.splitFile is not set, Split SDK maintains backward compatibility by trying to load the legacy file (.splits), now deprecated. In this mode, the SDK loads a local file called *localhost.splits* which has the following line format: +If SplitClientConfig.splitFile is not set, the SDK maintains backward compatibility by trying to load the legacy file (.splits), now deprecated. In this mode, the SDK loads a local file called *localhost.splits* which has the following line format: Starting from version 2.24.2, it is possible to update feature flag definitions programmatically by using the Localhost factory's `updateLocalhost` method, as shown below. ```swift title="Swift" -// Split SDK key must be "localhost" +// SDK key must be "localhost" let apiKey: String = "localhost" let key: Key = Key(matchingKey: "key") let config = SplitClientConfig() @@ -617,7 +617,7 @@ By enabling debug mode, the *localhost* file location is logged to the console s ## Listener -Split SDKs send impression data back to Split servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression handler*. +FME SDKs send impression data back to Harness servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression handler*. The SDK sends the generated impressions to the impression handler right away. As a result, be careful while implementing handling logic to avoid blocking the main thread. Generally speaking, you should create a separate thread to handle incoming impressions. Refer to the snippet below. @@ -663,7 +663,7 @@ In regards with the data available here, refer to the `impression` objects inter ## Flush -The flush() method sends the data stored in memory (impressions and events) to Split cloud and clears the successfully posted data. If a connection issue is experienced, the data will be sent on the next attempt. +The flush() method sends the data stored in memory (impressions and events) to Harness FME servers and clears the successfully posted data. If a connection issue is experienced, the data will be sent on the next attempt. ```swift title="Swift" client.flush() @@ -675,7 +675,7 @@ Since version 2.11.0, background synchronization is available for devices having To enable this feature, just follow the next 4 steps: 1. Enable _[Background Mode Fetch](https://developer.apple.com/documentation/watchkit/background_execution/enabling_background_sessions)_ capability for your app. -2. Add the Split SDK background sync task identifier *io.split.bg-sync.task* to the Permitted background task scheduler identifiers section of the Info.plist . +2. Add the SDK background sync task identifier *io.split.bg-sync.task* to the Permitted background task scheduler identifiers section of the Info.plist . 3. Set the Split config flag _synchronizeInBackground_ to true . ```swift title="Swift" @@ -722,7 +722,7 @@ This section describes advanced use cases and features provided by the SDK. ### Instantiate multiple SDK clients -In versions previous to 2.14.0, you had to create more that one SDK instance to evaluate for different users IDs. From v2.14.0 on, Split supports the ability to create multiple clients, one for each user ID. For example, if you need to roll out feature flags for different user IDs, you can instantiate multiple clients, one for each ID. You can then evaluate them using the corresponding client. You can do this using the example below: +In versions previous to 2.14.0, you had to create more that one SDK instance to evaluate for different users IDs. From v2.14.0 on, FME supports the ability to create multiple clients, one for each user ID. For example, if you need to roll out feature flags for different user IDs, you can instantiate multiple clients, one for each ID. You can then evaluate them using the corresponding client. You can do this using the example below: ```swift title="Swift" // Create factory @@ -763,9 +763,9 @@ While the SDK does not put any limitations on the number of instances that you c You can listen for four different events from the SDK. * `sdkReadyFromCache`. This event fires once the SDK is ready to evaluate treatments using a locally cached version of your rollout plan from a previous session (which might be stale). If there is data in the cache, this event fires almost immediately, since access to the cache is fast; otherwise, it doesn't fire. -* ` sdkReady`. This event fires once the SDK is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Split servers. -* ` sdkReadyTimedOut`. This event fires if there is no cached version of your rollout plan in disk cache, and the SDK could not fully download the data from Split servers within the time specified by the `sdkReadyTimeOut` property of the `SplitClientConfig` object. This event does not indicate that the SDK initialization was interrupted. The SDK continues downloading the rollout plan and fires the `sdkReady` event when finished. This delayed `sdkReady` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. -* `sdkUpdated`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in the Split user interface. +* ` sdkReady`. This event fires once the SDK is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Harness servers. +* ` sdkReadyTimedOut`. This event fires if there is no cached version of your rollout plan in disk cache, and the SDK could not fully download the data from Harness servers within the time specified by the `sdkReadyTimeOut` property of the `SplitClientConfig` object. This event does not indicate that the SDK initialization was interrupted. The SDK continues downloading the rollout plan and fires the `sdkReady` event when finished. This delayed `sdkReady` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. +* `sdkUpdated`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in Harness FME. SDK event handling is done through the function `on(event:execute:)`, which receives a closure as an event handler. @@ -792,7 +792,7 @@ client.on(event: SplitEvent.sdkReadyTimedOut) { } client.on(event: SplitEvent.sdkReadyFromCache) { - // Fired after the SDK could confirm the presence of the Split data. + // Fired after the SDK could confirm the presence of the FME data. // This event fires quickly, since there's no actual fetching of information. // Keep in mind that data might be stale, this is NOT a replacement of sdkReady. @@ -816,8 +816,8 @@ The SDK allows you to disable the tracking of events and impressions until user The `userConsent` configuration parameter lets you set the initial consent status of the SDK instance, and the factory method `setUserConsent(enabled: Bool)` lets you grant (enable) or decline (disable) dynamic data tracking. There are three possible initial states: - * `'GRANTED'`: The user grants consent for tracking events and impressions. The SDK sends them to Split cloud. This is the default value if `userConsent` param is not defined. - * `'DECLINED'`: The user declines consent for tracking events and impressions. The SDK does not send them to Split cloud. + * `'GRANTED'`: The user grants consent for tracking events and impressions. The SDK sends them to Harness FME servers. This is the default value if `userConsent` param is not defined. + * `'DECLINED'`: The user declines consent for tracking events and impressions. The SDK does not send them to Harness FME servers. * `'UNKNOWN'`: The user neither grants nor declines consent for tracking events and impressions. The SDK tracks them in its internal storage, and eventually either sends them or not if the consent status is updated to `'GRANTED'` or `'DECLINED'` respectively. The status can be updated at any time with the `setUserConsent` factory method. @@ -829,7 +829,7 @@ Working with user consent is demonstrated below. // Overwrites the initial consent status of the factory instance, which is 'GRANTED' by default. // 'UNKNOWN' status represents that the user has neither granted nor declined consent for tracking data, - // so the SDK locally tracks data but not send it to Split cloud until consent is changed to 'GRANTED'. + // so the SDK locally tracks data but not send it to Harness FME servers until consent is changed to 'GRANTED'. config.userConsent = .unknown guard let factory = DefaultSplitFactoryBuilder() .setApiKey("YOUR_SDK_KEY") @@ -839,9 +839,9 @@ Working with user consent is demonstrated below. return } - // Changed User Consent status to 'GRANTED'. Data will be sent to Split cloud. + // Changed User Consent status to 'GRANTED'. Data will be sent to Harness FME servers. factory.setUserConsent(enabled: true); - // Changed User Consent status to 'DECLINED'. Data will not be sent to Split cloud. + // Changed User Consent status to 'DECLINED'. Data will not be sent to Harness FME servers. factory.setUserConsent(enabled: false); // The 'getUserConsent' method returns User Consent status. @@ -885,7 +885,7 @@ certBuilder.certificatePinningConfig { host in print("Failed validation for host \(host)") } -// Set the CertificatePinningConfig property for the Split client configuration +// Set the CertificatePinningConfig property for the SDK factory client configuration let config = SplitClientConfig() config.certificatePinningConfig = certBuilder.build() // you can add other configuration properties here diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/javascript-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/javascript-sdk.md index 2035d8c572b..95d3cc5a0ae 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/javascript-sdk.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/javascript-sdk.md @@ -15,7 +15,7 @@ import TabItem from '@theme/TabItem'; This guide provides detailed information about our JavaScript SDK. All of our SDKs are open source. Go to our [JavaScript SDK GitHub repository](https://github.com/splitio/javascript-client) to see the source code. :::info[Migrating from v10.x to v11.x] -When upgrading, consider that the traffic type is no longer configured for the SDK client, and must be sent on the `client.track()` method call instead. +When upgrading, consider that the traffic type is no longer configured for the SDK factory client, and must be sent on the `client.track()` method call instead. Refer to the [migration guide](https://github.com/splitio/javascript-client/blob/development/MIGRATION-GUIDE.md) for complete information on upgrading to v11.x. ::: @@ -28,7 +28,7 @@ If you're looking for possible polyfill options, check [es6-promise](https://git ## Initialization -Set up Split in your code base with two simple steps. +Set up FME in your code base with two simple steps. ### 1. Import the SDK into your project @@ -47,7 +47,7 @@ npm install --save @splitsoftware/splitio
-### 2. Instantiate the SDK and create a new Split client +### 2. Instantiate the SDK and create a new SDK factory client @@ -95,11 +95,6 @@ import { SplitFactory } from '@splitsoftware/splitio'; const factory: SplitIO.IBrowserSDK = SplitFactory({ core: { authorizationKey: 'YOUR_SDK_KEY', - // the key can be the logged in - // user id, or the account id that - // the logged in user belongs to. - // The type of customer (user, account, custom) - // is chosen during Split's sign-up process. key: 'key' }, startup: { @@ -124,20 +119,20 @@ With the SDK package on NPM, you get the SplitIO namespace, which contains usefu Feel free to dive into the declaration files if IntelliSense is not enough. ::: -We recommend instantiating the Split factory once as a singleton and reusing it throughout your application. +We recommend instantiating the SDK factory once as a singleton and reusing it throughout your application. -Configure the SDK with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split UI, on your Admin settings page, API keys section. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. +Configure the SDK with the SDK key for the FME environment that you would like to access. The SDK key is available in Harness FME Admin settings. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. ## Using the SDK ### Basic use -When the SDK is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds, depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while it's in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns the [control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). +When the SDK is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Harness servers. This process can take up to a few hundred milliseconds, depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while it's in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns the [control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). To make sure the SDK properly loads before asking it for a treatment, block until the SDK is ready, as shown below. We set the client to listen for the `SDK_READY` event triggered by the SDK before asking for an evaluation. -After the `SDK_READY` event fires, you can use the `getTreatment` method to return the proper treatment based on the feature flag name and the key you passed when instantiating the SDK. Then use an if-else-if block as shown below and insert the code for the different treatments that you defined in the Split user interface. Remember the final else branch in your code to handle the client returning control. +After the `SDK_READY` event fires, you can use the `getTreatment` method to return the proper treatment based on the feature flag name and the key you passed when instantiating the SDK. Then use an if-else-if block as shown below and insert the code for the different treatments that you defined in Harness FME. Remember the final else branch in your code to handle the client returning control. @@ -176,7 +171,7 @@ client.on(client.Event.SDK_READY, function() { To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK's `getTreatment` method needs to be passed an attribute map at runtime. -In the example below, we are rolling out a feature to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in the Split user interface to decide whether to show the `on` or `off` treatment to this account. +In the example below, we are rolling out a feature to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in Harness FME to decide whether to show the `on` or `off` treatment to this account. The `getTreatment` method has a number of variations that are described below. Each of these additionally has a variation that takes an attributes argument, which can defines attributes of the following types: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: @@ -286,7 +281,7 @@ var result = client.clearAttributes(); ### Multiple evaluations at once -In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` from the Split client to do this. +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` from the SDK factory client to do this. * `getTreatments`: Pass a list of the feature flag names you want treatments for. * `getTreatmentsByFlagSet`: Evaluate all flags that are part of the provided set name and are cached on the SDK instance. * `getTreatmentsByFlagSets`: Evaluate all flags that are part of the provided set names and are cached on the SDK instance. @@ -359,7 +354,7 @@ type TreatmentResult = { -As you can see from the object structure, the config is a stringified version of the configuration JSON defined in the Split user interface. If there is no configuration defined for a treatment, the SDK returns `null` for the config parameter. +As you can see from the object structure, the config is a stringified version of the configuration JSON defined in Harness FME. If there is no configuration defined for a treatment, the SDK returns `null` for the config parameter. This method takes the exact same set of arguments as the standard `getTreatment` method. Refer to the examples below for proper usage: @@ -449,7 +444,7 @@ treatmentResults = client.getTreatmentsWithConfigByFlagSets(flagSets); ### Shutdown -Call the `client.destroy()` method before letting a process using the SDK exit, as this method gracefully shuts down the Split SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. +Call the `client.destroy()` method before letting a process using the SDK exit, as this method gracefully shuts down the SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. ```javascript title="JavaScript" // You can just destroy and remove the variable reference and move on: @@ -474,22 +469,22 @@ A call to the `destroy()` method also destroys the factory object. When creating ## Track -Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your features on your users’ actions and metrics. +Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Harness FME and allows you to measure the impact of your features on your users’ actions and metrics. -Learn more about [tracking events](https://help.split.io/hc/en-us/articles/360020585772) in Split. +Learn more about [tracking events](https://help.split.io/hc/en-us/articles/360020585772). In the examples below you can see that the `.track()` method can take up to four arguments. The proper data type and syntax for each are: -* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined Harness FME. * **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: * Contains 63 characters or fewer. * Starts with a letter or number. * Contains only letters, numbers, hyphen, underscore, or period. * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` * **VALUE:** (Optional) The value to be used in creating the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is **Integer** or **Float**. -* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. +* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. FME currently supports three types of properties: strings, numbers, and booleans. -The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event to be sent back to Split's servers on the next event post. The SDK will return `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method has been provided. +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event to be sent back to Harness servers on the next event post. The SDK will return `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method has been provided. In the case that a bad input has been provided, you can read more about our SDK's expected behavior [here](https://help.split.io/hc/en-us/articles/360020585772-Track-events) @@ -544,21 +539,21 @@ The SDK has a number of knobs for configuring performance. Each knob is tuned to | **Configuration** | **Description** | **Default value** | | --- | --- | --- | -| core.labelsEnabled | Enable impression labels from being sent to Split backend. Labels may contain sensitive information. | true | +| core.labelsEnabled | Enable impression labels from being sent to Harness servers. Labels may contain sensitive information. | true | | startup.readyTimeout | Maximum amount of time in seconds to wait before firing the `SDK_READY_TIMED_OUT` event | 10 | | startup.requestTimeoutBeforeReady | The SDK has two main endpoints it uses /splitChanges and /mySegments that it hits to get ready. This config sets how long (in seconds) the SDK waits for each request it makes as part of getting ready. | 5 | | startup.retriesOnFailureBeforeReady | How many retries on /splitChanges and /mySegments we do while getting the SDK ready | 1 | | startup.eventsFirstPushWindow | Use to set a specific timer (expressed in seconds) for the first push of events, starting on SDK initialization. | 10 | -| scheduler.featuresRefreshRate | The SDK polls Split servers for changes to feature rollout plans. This parameter controls this polling period in seconds. | 60 | -| scheduler.segmentsRefreshRate | The SDK polls Split servers for changes to segment definitions. This parameter controls this polling period in seconds. | 60 | -| scheduler.impressionsRefreshRate | The SDK sends information on who got what treatment at what time back to Split servers to power analytics. This parameter controls how often this data is sent to Split servers. The parameter should be in seconds. | 300 | +| scheduler.featuresRefreshRate | The SDK polls Harness servers for changes to feature rollout plans. This parameter controls this polling period in seconds. | 60 | +| scheduler.segmentsRefreshRate | The SDK polls Harness servers for changes to segment definitions. This parameter controls this polling period in seconds. | 60 | +| scheduler.impressionsRefreshRate | The SDK sends information on who got what treatment at what time back to Harness servers to power analytics. This parameter controls how often this data is sent to Harness servers. The parameter should be in seconds. | 300 | | scheduler.impressionsQueueSize | The max amount of impressions we queue. If the queue is full, the SDK flushes the impressions and resets the timer. | 30000 | -| scheduler.eventsPushRate | The SDK sends tracked events to Split servers. This setting controls that flushing rate in seconds. | 60 | +| scheduler.eventsPushRate | The SDK sends tracked events to Harness servers. This setting controls that flushing rate in seconds. | 60 | | scheduler.eventsQueueSize | The max amount of events we queue. If the queue is full, the SDK flushes the events and resets the timer. | 500 | -| scheduler.telemetryRefreshRate | The SDK caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers (in seconds). | 3600 seconds (1 hour) | +| scheduler.telemetryRefreshRate | The SDK caches diagnostic data that it periodically sends to Harness servers. This configuration controls how frequently this data is sent back to Harness servers (in seconds). | 3600 seconds (1 hour) | | sync.splitFilters | Filter specific feature flags to be synced and evaluated by the SDK. This is formed by a type string property and a list of string values for the given criteria. Using the types 'bySet' (recommended, flag sets are available in all tiers) or 'byName', pass an array of strings defining the query. If empty or unset, all feature flags are downloaded by the SDK. | [] | -| sync.impressionsMode | This configuration defines how impressions (decisioning events) are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Split; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, ALL impressions are queued and sent to Split; this is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. | `OPTIMIZED` | -| sync.enabled | Controls the SDK continuous synchronization flags. When `true`, a running SDK processes rollout plan updates performed in the Split user interface (default). When `false`, it fetches all data from the Split cloud only upon init, which ensures a consistent experience during a user session and optimizes resources when these updates are not consumed by the app. | true | +| sync.impressionsMode | This configuration defines how impressions (decisioning events) are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Harness; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Harness FME and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, ALL impressions are queued and sent to Harness; this is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. | `OPTIMIZED` | +| sync.enabled | Controls the SDK continuous synchronization flags. When `true`, a running SDK processes rollout plan updates performed in Harness FME (default). When `false`, it fetches all data from the Harness FME servers only upon init, which ensures a consistent experience during a user session and optimizes resources when these updates are not consumed by the app. | true | | sync.requestOptions.getHeaderOverrides | A callback function that can be used to override the Authentication header or append new headers to the SDK's HTTP(S) requests. | undefined | | storage.type | Storage type to be used by the SDK. Possible values are `MEMORY` and `LOCALSTORAGE`. | `MEMORY` | | storage.prefix | An optional prefix for your data to avoid collisions. This prefix is prepended to the existing SPLITIO localStorage prefix. | `SPLITIO` | @@ -649,7 +644,7 @@ const sdk: SplitIO.IBrowserSDK = SplitFactory({ ## Localhost mode -For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the Split SDK can be started in **localhost** mode (aka off-the-grid or offline mode). In this mode, the SDK neither polls nor updates Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the features. +For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the SDK can be started in **localhost** mode (aka off-the-grid or offline mode). In this mode, the SDK neither polls nor updates Harness servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the features. When instantiating the SDK in localhost mode, your `authorizationKey` is `localhost`. Define the feature flags you want to use in the `features` object map. All `getTreatment` calls for a feature flag now only return the one treatment (and config, if defined) that you have defined in the map. @@ -767,7 +762,7 @@ config.features = { 'reporting_v3': 'off' }; // Will not emit SDK_UPDATE ## Manager -Use the Split manager to get a list of features available to the Split client. To instantiate a Manager in your code base, use the same factory that you used for your client. +Use the Split manager to get a list of features available to the SDK factory client. To instantiate a Manager in your code base, use the same factory that you used for your client. @@ -775,11 +770,6 @@ Use the Split manager to get a list of features available to the Split client. T var factory = SplitFactory({ core: { authorizationKey: 'YOUR_SDK_KEY', - // the key can be the logged in - // user id, or the account id that - // the logged in user belongs to. - // The type of customer (user, account, custom) - // is chosen during Split's sign-up process. key: 'key' } }); @@ -796,11 +786,6 @@ manager.once(manager.Event.SDK_READY, function() { const factory: SplitIO.IBrowserSDK = SplitFactory({ core: { authorizationKey: 'YOUR_SDK_KEY', - // the key can be the logged in - // user id, or the account id that - // the logged in user belongs to. - // The type of customer (user, account, custom) - // is chosen during Split's sign-up process. key: 'key' } }); @@ -887,7 +872,7 @@ type SplitView = { ## Listener -Split SDKs send impression data back to Split servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression listener*. For that purpose, the SDK's configurations have a parameter called `impressionListener` where an implementation of `ImpressionListener` could be added. This implementation **must** define the `logImpression` method and it receives data in the following schema. +FME SDKs send impression data back to Harness servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression listener*. For that purpose, the SDK's configurations have a parameter called `impressionListener` where an implementation of `ImpressionListener` could be added. This implementation **must** define the `logImpression` method and it receives data in the following schema. | **Name** | **Type** | **Description** | | --- | --- | --- | @@ -1027,9 +1012,9 @@ This section describes advanced use cases and features provided by the SDK. ### Instantiate multiple SDK clients -Each JavaScript SDK client is tied to one specific customer and traffic type at a time (for example, `user`, `account`, `organization`). This enhances performance and reduces data cached within the SDK. +Each JavaScript SDK factory client is tied to one specific customer and traffic type at a time (for example, `user`, `account`, `organization`). This enhances performance and reduces data cached within the SDK. -Split supports the ability to release based on multiple traffic types. With traffic types, you can release to `users` in one feature flag and `accounts` in another. If you are unfamiliar with using multiple traffic types, refer to [Traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) for more information. +FME supports the ability to release based on multiple traffic types. With traffic types, you can release to `users` in one feature flag and `accounts` in another. If you are unfamiliar with using multiple traffic types, refer to [Traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) for more information. If you need to roll out features by different traffic types, instantiate multiple SDK clients, one for each traffic type. For example, you may want to roll out the feature `user-poll` by `users` and the feature `account-permissioning` by `accounts`. You can do this with the example below: @@ -1110,9 +1095,9 @@ While the SDK does not put any limitations on the number of instances that can b You can listen for four different events from the SDK. * `SDK_READY_FROM_CACHE`. This event fires once the SDK is ready to evaluate treatments using a version of your rollout plan cached in localStorage from a previous session (which might be stale). If there is data in localStorage, this event fires almost immediately, since access to localStorage is fast; otherwise, it doesn't fire. -* `SDK_READY`. This event fires once the SDK is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Split servers. -* `SDK_READY_TIMED_OUT`. This event fires if there is no cached version of your rollout plan cached in localStorage, and the SDK could not download the data from Split servers within the time specified by the `readyTimeout` configuration parameter. This event does not indicate that the SDK initialization was interrupted. The SDK continues downloading the rollout plan and fires the `SDK_READY` event when finished. This delayed `SDK_READY` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. -* `SDK_UPDATE`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in the Split user interface. +* `SDK_READY`. This event fires once the SDK is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Harness servers. +* `SDK_READY_TIMED_OUT`. This event fires if there is no cached version of your rollout plan cached in localStorage, and the SDK could not download the data from Harness servers within the time specified by the `readyTimeout` configuration parameter. This event does not indicate that the SDK initialization was interrupted. The SDK continues downloading the rollout plan and fires the `SDK_READY` event when finished. This delayed `SDK_READY` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. +* `SDK_UPDATE`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in Harness FME. The syntax to listen for each event is shown below: @@ -1146,9 +1131,9 @@ client.on(client.Event.SDK_UPDATE, function () { console.log('The SDK has been updated!'); }); -// This event only fires using the LocalStorage option and if there's Split data stored in the browser. +// This event only fires using the LocalStorage option and if there's FME data stored in the browser. client.once(client.Event.SDK_READY_FROM_CACHE, function () { - // Fired after the SDK could confirm the presence of the Split data. + // Fired after the SDK could confirm the presence of the FME data. // This event fires really quickly, since there's no actual fetching of information. // Keep in mind that data might be stale, this is NOT a replacement of SDK_READY. }); @@ -1183,9 +1168,9 @@ client.on(client.Event.SDK_UPDATE, () => { console.log('The SDK has been updated!'); }); -// This event only fires using the LocalStorage option and if there's Split data stored in the browser. +// This event only fires using the LocalStorage option and if there's FME data stored in the browser. client.once(client.Event.SDK_READY_FROM_CACHE, function () { - // Fired after the SDK could confirm the presence of the Split data. + // Fired after the SDK could confirm the presence of the FME data. // This event fires really quickly, since there's no actual fetching of information. // Keep in mind that data might be stale, this is NOT a replacement of SDK_READY. }); @@ -1200,8 +1185,8 @@ The SDK allows you to disable the tracking of events and impressions until user The `userConsent` configuration parameter lets you set the initial consent status of the SDK instance, and the factory method `UserConsent.setStatus(boolean)` lets you grant (enable) or decline (disable) dynamic data tracking. There are three possible initial states: - * `'GRANTED'`: The user grants consent for tracking events and impressions. The SDK sends them to Split cloud. This is the default value if `userConsent` param is not defined. - * `'DECLINED'`: The user declines consent for tracking events and impressions. The SDK does not send them to Split cloud. + * `'GRANTED'`: The user grants consent for tracking events and impressions. The SDK sends them to Harness FME servers. This is the default value if `userConsent` param is not defined. + * `'DECLINED'`: The user declines consent for tracking events and impressions. The SDK does not send them to Harness FME servers. * `'UNKNOWN'`: The user neither grants nor declines consent for tracking events and impressions. The SDK tracks them in its internal storage, and eventually either sends them or not if the consent status is updated to `'GRANTED'` or `'DECLINED'` respectively. The status can be updated at any time with the `UserConsent.setStatus` factory method. @@ -1216,7 +1201,7 @@ var factory = SplitFactory({ }, // Overwrites the initial consent status of the factory instance, which is 'GRANTED' by default. // 'UNKNOWN' status represents that the user has neither granted nor declined consent for tracking data, - // so the SDK will locally track data but not send it to Split cloud until consent is changed to 'GRANTED'. + // so the SDK will locally track data but not send it to Harness FME servers until consent is changed to 'GRANTED'. userConsent: 'UNKNOWN' }); @@ -1225,16 +1210,16 @@ factory.UserConsent.getStatus() === factory.UserConsent.Status.UNKNOWN; // `setStatus` method lets you update the factory consent status at any time. // Pass `true` for 'GRANTED' and `false` for 'DECLINED'. -factory.UserConsent.setStatus(true); // Consent status changed from 'UNKNOWN' to 'GRANTED'. Data will be sent to Split cloud. +factory.UserConsent.setStatus(true); // Consent status changed from 'UNKNOWN' to 'GRANTED'. Data will be sent to Harness FME servers. factory.UserConsent.getStatus() === factory.UserConsent.Status.GRANTED; -factory.UserConsent.setStatus(false); // Consent status changed from 'GRANTED' to 'DECLINED'. Data will not be sent to Split cloud. +factory.UserConsent.setStatus(false); // Consent status changed from 'GRANTED' to 'DECLINED'. Data will not be sent to Harness FME servers. factory.UserConsent.getStatus() === factory.UserConsent.Status.DECLINED; ``` ## Example apps -The following example applications detail how to configure and instantiate the Split JavaScript SDK on commonly used platforms: +The following example applications detail how to configure and instantiate the JavaScript SDK on commonly used platforms: * [Basic HTML](https://github.com/splitio/example-javascript-client) * [AngularJS](https://github.com/splitio/angularjs-sdk-examples) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/react-native-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/react-native-sdk.md index c7f3c5a3d6e..e4180e816e9 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/react-native-sdk.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/react-native-sdk.md @@ -24,13 +24,13 @@ Refer to this [migration guide](https://github.com/splitio/react-native-client/b ## Language support -The Split SDK for React Native supports both React Native bare projects (using [React-Native CLI](https://reactnative.dev/docs/environment-setup)) and Expo managed projects (using [Expo CLI](https://docs.expo.io/get-started/installation/)). +The FME SDK for React Native supports both React Native bare projects (using [React-Native CLI](https://reactnative.dev/docs/environment-setup)) and Expo managed projects (using [Expo CLI](https://docs.expo.io/get-started/installation/)). It has been validated with React Native v0.59 and later, and Expo v36 and later, but should also work with older versions. ## Initialization -Set up Split in your code base with two steps. +Set up FME in your code base with two steps. ### 1. Import the SDK into your project @@ -54,7 +54,7 @@ expo install @splitsoftware/splitio-react-native -The Split SDKs support two synchronization mechanisms, **streaming** (default and recommended) and **polling** which is the fallback in cases where streaming is not supported or as a temporary measure in case of any issues detected on the persistent connection. We recommend following the steps below to enable the necessary support for the Event Source modules. +The SDK supports two synchronization mechanisms, **streaming** (default and recommended) and **polling** which is the fallback in cases where streaming is not supported or as a temporary measure in case of any issues detected on the persistent connection. We recommend following the steps below to enable the necessary support for the Event Source modules. - For React Native bare projects, you need to *link* the native modules of the package. @@ -62,7 +62,7 @@ If using React Native 0.59 or below, run `react-native link @splitsoftware/split If using React Native 0.60+, the [autolink feature](https://github.com/react-native-community/cli/blob/master/docs/autolinking.md) is available and you don't need to run `react-native link`, but you still need to install the pods if developing for iOS, with the command `npx pod-install ios`. -- For Expo managed projects, Split SDK native modules cannot be used, but you can still support streaming by *polyfilling* the global EventSource constructor: +- For Expo managed projects, SDK native modules cannot be used, but you can still support streaming by *polyfilling* the global EventSource constructor: Install an EventSource implementation such as [react-native-event-source](https://www.npmjs.com/package/react-native-event-source): @@ -78,7 +78,7 @@ import RNEventSource from 'react-native-event-source'; globalThis.EventSource = RNEventSource; ``` -### 2. Instantiate the SDK and create a new Split client +### 2. Instantiate the SDK and create a new SDK factory client @@ -127,21 +127,21 @@ With the SDK package you get the SplitIO namespace, which contains useful types Feel free to dive into the declaration files if IntelliSense is not enough! ::: -We recommend instantiating the Split factory once as a singleton and reusing it throughout your application. Consider instantiating it once in the global scope, or in the `componentDidMount` method of your application root component. +We recommend instantiating the SDK factory once as a singleton and reusing it throughout your application. Consider instantiating it once in the global scope, or in the `componentDidMount` method of your application root component. -Configure the SDK with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split UI, on your Admin settings page, API keys section. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. +Configure the SDK with the SDK key for the FME environment that you would like to access. The SDK key is available in Harness FME Admin settings. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. ## Using the SDK ### Basic use -When the SDK is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while its in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). +When the SDK is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Harness servers. This process can take up to a few hundred milliseconds depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while its in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). To make sure the SDK is properly loaded before asking it for a treatment, block until the SDK is ready, as shown below. We set the client to listen for the `SDK_READY` event triggered by the SDK before asking for an evaluation. After the `SDK_READY` event fires, you can use the `getTreatment` method to return the proper treatment based on the `FEATURE_FLAG_NAME` and the `key` variables you passed when instantiating the SDK. -Then use an if-else-if block as shown below and insert the code for the different treatments that you defined in the Split user interface. Remember the final else branch in your code to handle the client returning control. +Then use an if-else-if block as shown below and insert the code for the different treatments that you defined in Harness FME. Remember the final else branch in your code to handle the client returning control. @@ -181,7 +181,7 @@ When running your app in debug mode on an Android device or emulator, you might The warning is explained [here](https://github.com/facebook/react-native/issues/12981#issuecomment-652745831). It is intended to make developers aware that timer callbacks are invoked in foreground, and therefore timers could be delayed while the app is in background. -Since the SDK uses timers for periodically pushing data to Split cloud, it is acceptable if those operations are delayed while the app is in background, and so it is completely safe to ignore or [hide this warning](https://reactnative.dev/docs/debugging#console-errors-and-warnings). If there is any concern, feel free to contact us through support. +Since the SDK uses timers for periodically pushing data to Harness FME servers, it is acceptable if those operations are delayed while the app is in background, and so it is completely safe to ignore or [hide this warning](https://reactnative.dev/docs/debugging#console-errors-and-warnings). If there is any concern, feel free to contact us through support. ```javascript import { LogBox } from 'react-native'; @@ -194,7 +194,7 @@ LogBox.ignoreLogs(['Setting a timer']); To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK's `getTreatment` method needs to be passed an attribute map at runtime. -In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in the Split user interface to decide whether to show the `on` or `off` treatment to this account. +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in Harness FME to decide whether to show the `on` or `off` treatment to this account. The `getTreatment` method has a number of variations that are described below. Each of these additionally has a variation that takes an attributes argument, which can defines attributes of the following types: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: @@ -310,7 +310,7 @@ var result = client.clearAttributes(); ### Multiple evaluations at once -In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` from the Split client to do this. +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` from the SDK factory client to do this. * `getTreatments`: Pass a list of the feature flag names you want treatments for. * `getTreatmentsByFlagSet`: Evaluate all flags that are part of the provided set name and are cached on the SDK instance. * `getTreatmentsByFlagSets`: Evaluate all flags that are part of the provided set names and are cached on the SDK instance. @@ -385,7 +385,7 @@ type TreatmentResult = { -As you can see from the object structure, the config will be a stringified version of the configuration JSON defined in the Split user interface. If there is no configuration defined for a treatment, the SDK will return `null` for the config parameter. +As you can see from the object structure, the config will be a stringified version of the configuration JSON defined in Harness FME. If there is no configuration defined for a treatment, the SDK will return `null` for the config parameter. This method takes the exact same set of arguments as the standard `getTreatment` method. See below for examples on proper usage: @@ -477,7 +477,7 @@ treatmentResults = client.getTreatmentsWithConfigByFlagSets(flagSets); ### Shutdown -You can call the `client.destroy()` method to gracefully shut down the Split SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. +You can call the `client.destroy()` method to gracefully shut down the SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. If the SDK was instantiated in the `componentDidMount` method of a React component, `destroy` should be called in the corresponding `componentWillUnmount` method. However while releasing resources if the SDK is not needed anymore is a good practice, since the SDK automatically hooks to application state transitions (foreground, background) data synchronization is managed by the SDK and pending events are flushed automatically. @@ -510,22 +510,22 @@ A call to the `destroy()` method also destroys the factory object. When creating ## Track -Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users’ actions and metrics. +Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Harness FME and allows you to measure the impact of your feature flags on your users’ actions and metrics. [Learn more](https://help.split.io/hc/en-us/articles/360020585772) about using track events in feature flags. In the examples below you can see that the `.track()` method can take up to four arguments. The proper data type and syntax for each are: -* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined Harness FME. * **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: * Contains 63 characters or fewer. * Starts with a letter or number. * Contains only letters, numbers, hyphen, underscore, or period. * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` * **VALUE:** (Optional) The value to be used in creating the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is **Integer** or **Float**. -* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. +* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. FME currently supports three types of properties: strings, numbers, and booleans. -The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event to be sent back to Split's servers on the next event post. The SDK returns `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method has been provided. +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event to be sent back to Harness servers on the next event post. The SDK returns `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method has been provided. In the case that a bad input has been provided, you can read more about our SDK's expected behavior [here](https://help.split.io/hc/en-us/articles/360020585772-Track-events) @@ -567,21 +567,21 @@ The SDK has a number of knobs for configuring performance. Each knob is tuned to | **Configuration** | **Description** | **Default value** | | --- | --- | --- | -| core.labelsEnabled | Enable impression labels from being sent to Split's backend. Labels may contain sensitive information. | true | +| core.labelsEnabled | Enable impression labels from being sent to Harness FME's backend. Labels may contain sensitive information. | true | | startup.readyTimeout | Maximum amount of time in seconds to wait before firing the `SDK_READY_TIMED_OUT` event | 10 | | startup.requestTimeoutBeforeReady | The SDK has two main endpoints it uses /splitChanges and /mySegments that it hits to get ready. This config sets how long (in seconds) the SDK will wait for each request it makes as part of getting ready. | 5 | | startup.retriesOnFailureBeforeReady | How many retries on /splitChanges and /mySegments we will do while getting the SDK ready | 1 | | startup.eventsFirstPushWindow | Use to set a specific timer (expressed in seconds) for the first push of events, starting on SDK initialization. | 10 | -| scheduler.featuresRefreshRate | The SDK polls Split servers for changes to feature rollout plans. This parameter controls this polling period in seconds. | 60 | -| scheduler.segmentsRefreshRate | The SDK polls Split servers for changes to segment definitions. This parameter controls this polling period in seconds. | 60 | -| scheduler.impressionsRefreshRate | The SDK sends information on who got what treatment at what time back to Split servers to power analytics. This parameter controls how often this data is sent to Split servers. The parameter should be in seconds. | 300 | +| scheduler.featuresRefreshRate | The SDK polls Harness servers for changes to feature rollout plans. This parameter controls this polling period in seconds. | 60 | +| scheduler.segmentsRefreshRate | The SDK polls Harness servers for changes to segment definitions. This parameter controls this polling period in seconds. | 60 | +| scheduler.impressionsRefreshRate | The SDK sends information on who got what treatment at what time back to Harness servers to power analytics. This parameter controls how often this data is sent to Harness servers. The parameter should be in seconds. | 300 | | scheduler.impressionsQueueSize | The max amount of impressions we queue. If the queue is full, the SDK flushes the impressions and resets the timer. | 30000 | -| scheduler.eventsPushRate | The SDK sends tracked events to Split servers. This setting controls that flushing rate in seconds. | 60 | +| scheduler.eventsPushRate | The SDK sends tracked events to Harness servers. This setting controls that flushing rate in seconds. | 60 | | scheduler.eventsQueueSize | The max amount of events we queue. If the queue is full, the SDK flushes the events and resets the timer. | 500 | -| scheduler.telemetryRefreshRate | The SDK caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers (in seconds). | 3600 seconds (1 hour) | +| scheduler.telemetryRefreshRate | The SDK caches diagnostic data that it periodically sends to Harness servers. This configuration controls how frequently this data is sent back to Harness servers (in seconds). | 3600 seconds (1 hour) | | sync.splitFilters | Filter specific feature flags to be synced and evaluated by the SDK. This is formed by a type string property and a list of string values for the given criteria. Using the types 'bySet' (recommended, flag sets are available in all tiers) or 'byName', pass an array of strings defining the query. If empty or unset, all feature flags are downloaded by the SDK. | [] | -| sync.impressionsMode | This configuration defines how impressions (decisioning events) are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Split; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, ALL impressions are queued and sent to Split; this is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. | `OPTIMIZED` | -| sync.enabled | Controls the SDK continuous synchronization flags. When `true`, a running SDK processes rollout plan updates performed in the Split user interface (default). When `false`, it fetches all data upon init, which ensures a consistent experience during a user session and optimizes resources when these updates are not consumed by the app. | true | +| sync.impressionsMode | This configuration defines how impressions (decisioning events) are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Harness; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Harness FME and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, ALL impressions are queued and sent to Harness; this is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. | `OPTIMIZED` | +| sync.enabled | Controls the SDK continuous synchronization flags. When `true`, a running SDK processes rollout plan updates performed in Harness FME (default). When `false`, it fetches all data upon init, which ensures a consistent experience during a user session and optimizes resources when these updates are not consumed by the app. | true | | sync.requestOptions.getHeaderOverrides | A callback function that can be used to override the Authentication header or append new headers to the SDK's HTTP(S) requests. | undefined | | debug | Either a boolean flag, string log level or logger instance for activating SDK logs. See [logging](#logging) for details. | false | | streamingEnabled | Boolean flag to enable the streaming service as default synchronization mechanism. In the event of an issue with streaming, the SDK will fallback to the polling mechanism. If false, the SDK will poll for changes as usual without attempting to use streaming. | true | @@ -659,7 +659,7 @@ const sdk: SplitIO.IBrowserSDK = SplitFactory({ ## Localhost mode -For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the Split SDK can be started in **localhost** mode (aka off-the-grid or offline mode). In this mode, the SDK neither polls nor updates Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the features. +For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the SDK can be started in **localhost** mode (aka off-the-grid or offline mode). In this mode, the SDK neither polls nor updates Harness servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the features. Define the feature flags you want to use in the `features` object map. All `getTreatment` calls for a feature flag now only return the one treatment (and config, if defined) that you have defined in the map. You can then change the treatment as necessary for your testing. To update a treatment or a config, or to add or remove feature flags from the mock cache, update the properties of the `features` object you've provided. The SDK simulates polling for changes and updates from it. Do not assign a new object to the `features` property because the SDK has a reference to the original object and will not detect the change. @@ -772,7 +772,7 @@ It is not recommended to use the default (online) mode of the SDK in your tests ## Manager -Use the Split Manager to get a list of features available to the Split client. +Use the Split Manager to get a list of features available to the SDK factory client. To instantiate a Manager in your code base, use the same factory that you used for your client. @@ -783,11 +783,6 @@ To instantiate a Manager in your code base, use the same factory that you used f var factory = SplitFactory({ core: { authorizationKey: 'YOUR_SDK_KEY', - // the key can be the logged in - // user id, or the account id that - // the logged in user belongs to. - // The type of customer (user, account, custom) - // is chosen during Split's sign-up process. key: 'key' } }); @@ -804,11 +799,6 @@ manager.once(manager.Event.SDK_READY, function() { const factory: SplitIO.IBrowserSDK = SplitFactory({ core: { authorizationKey: 'YOUR_SDK_KEY', - // the key can be the logged in - // user id, or the account id that - // the logged in user belongs to. - // The type of customer (user, account, custom) - // is chosen during Split's sign-up process. key: 'key' } }); @@ -898,7 +888,7 @@ type SplitView = { ## Listener -Split SDKs send impression data back to Split servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression listener*. For that purpose, the SDK's configurations have a parameter called `impressionListener` where an implementation of `ImpressionListener` could be added. This implementation **must** define the `logImpression` method and it receives data in the following schema. +FME SDKs send impression data back to Harness servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression listener*. For that purpose, the SDK's configurations have a parameter called `impressionListener` where an implementation of `ImpressionListener` could be added. This implementation **must** define the `logImpression` method and it receives data in the following schema. | **Name** | **Type** | **Description** | | --- | --- | --- | @@ -1042,9 +1032,9 @@ This section describes advanced use cases and features provided by the SDK. ### Instantiate multiple SDK clients -Split supports the ability to release based on multiple traffic types. For example, with traffic types, you can release to `users` in one feature flag and `accounts` in another. If you are unfamiliar with using multiple traffic types, refer to the [Traffic type guide](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) for more information. +FME supports the ability to release based on multiple traffic types. For example, with traffic types, you can release to `users` in one feature flag and `accounts` in another. If you are unfamiliar with using multiple traffic types, refer to the [Traffic type guide](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) for more information. -Each SDK client is tied to one specific customer ID at a time, so if you need to roll out feature flags by different traffic types, instantiate multiple SDK clients, one for each traffic type. For example, you may want to roll out the feature `user-poll` by `users` and the feature `account-permissioning` by `accounts`. +Each SDK factory client is tied to one specific customer ID at a time, so if you need to roll out feature flags by different traffic types, instantiate multiple SDK clients, one for each traffic type. For example, you may want to roll out the feature `user-poll` by `users` and the feature `account-permissioning` by `accounts`. You can do this with the example below. @@ -1128,9 +1118,9 @@ While the SDK does not put any limitations on the number of instances that can b You can listen for three different events from the SDK. -* `SDK_READY`. This event fires once the SDK is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Split servers. -* `SDK_READY_TIMED_OUT`. This event fires if the SDK could not download the data from Split servers within the time specified by the `readyTimeout` configuration parameter. This event does not indicate that the SDK initialization was interrupted. The SDK continues downloading the rollout plan and fires the `SDK_READY` event when finished. This delayed `SDK_READY` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. -* `SDK_UPDATE`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in the Split user interface. +* `SDK_READY`. This event fires once the SDK is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Harness servers. +* `SDK_READY_TIMED_OUT`. This event fires if the SDK could not download the data from Harness servers within the time specified by the `readyTimeout` configuration parameter. This event does not indicate that the SDK initialization was interrupted. The SDK continues downloading the rollout plan and fires the `SDK_READY` event when finished. This delayed `SDK_READY` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. +* `SDK_UPDATE`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in Harness FME. The syntax to listen for each event is shown below. @@ -1205,8 +1195,8 @@ The SDK allows you to disable the tracking of events and impressions until user The `userConsent` configuration parameter lets you set the initial consent status of the SDK instance, and the factory method `UserConsent.setStatus(boolean)` lets you grant (enable) or decline (disable) dynamic data tracking. There are three possible initial states: - * `'GRANTED'`: The user grants consent for tracking events and impressions. The SDK sends them to Split cloud. This is the default value if `userConsent` param is not defined. - * `'DECLINED'`: The user declines consent for tracking events and impressions. The SDK does not send them to Split cloud. + * `'GRANTED'`: The user grants consent for tracking events and impressions. The SDK sends them to Harness FME servers. This is the default value if `userConsent` param is not defined. + * `'DECLINED'`: The user declines consent for tracking events and impressions. The SDK does not send them to Harness FME servers. * `'UNKNOWN'`: The user neither grants nor declines consent for tracking events and impressions. The SDK tracks them in its internal storage, and eventually either sends them or not if the consent status is updated to `'GRANTED'` or `'DECLINED'` respectively. The status can be updated at any time with the `UserConsent.setStatus` factory method. @@ -1221,7 +1211,7 @@ var factory = SplitFactory({ }, // Overwrites the initial consent status of the factory instance, which is 'GRANTED' by default. // 'UNKNOWN' status represents that the user has neither granted nor declined consent for tracking data, - // so the SDK will locally track data but not send it to Split cloud until consent is changed to 'GRANTED'. + // so the SDK will locally track data but not send it to Harness FME servers until consent is changed to 'GRANTED'. userConsent: 'UNKNOWN' }); @@ -1230,17 +1220,17 @@ factory.UserConsent.getStatus() === factory.UserConsent.Status.UNKNOWN; // `setStatus` method lets you update the factory consent status at any time. // Pass `true` for 'GRANTED' and `false` for 'DECLINED'. -factory.UserConsent.setStatus(true); // Consent status changed from 'UNKNOWN' to 'GRANTED'. Data will be sent to Split cloud. +factory.UserConsent.setStatus(true); // Consent status changed from 'UNKNOWN' to 'GRANTED'. Data will be sent to Harness FME servers. factory.UserConsent.getStatus() === factory.UserConsent.Status.GRANTED; -factory.UserConsent.setStatus(false); // Consent status changed from 'GRANTED' to 'DECLINED'. Data will not be sent to Split cloud. +factory.UserConsent.setStatus(false); // Consent status changed from 'GRANTED' to 'DECLINED'. Data will not be sent to Harness FME servers. factory.UserConsent.getStatus() === factory.UserConsent.Status.DECLINED; ``` ### Usage with React SDK -The [Split React SDK](https://help.split.io/hc/en-us/articles/360038825091-React-SDK) is a wrapper around the Split JavaScript SDK that provides a more React-friendly API based on React components and hooks. You can use the React Native SDK with the React SDK in your React Native application following this [Usage Guide](https://help.split.io/hc/en-us/articles/360038825091-React-SDK#usage-with-react-native). +The [React SDK](https://help.split.io/hc/en-us/articles/360038825091-React-SDK) is a wrapper around the JavaScript SDK that provides a more React-friendly API based on React components and hooks. You can use the React Native SDK with the React SDK in your React Native application following this [Usage Guide](https://help.split.io/hc/en-us/articles/360038825091-React-SDK#usage-with-react-native). ## Example apps diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/react-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/react-sdk.md index 125e7b57fa9..c00c2490e26 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/react-sdk.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/react-sdk.md @@ -28,7 +28,7 @@ Although these components are still working, we recommend migrating to the `Spli ## Language support -The Split React SDK requires React 16.8.0 or above, since it uses **React Hooks API** introduced in that version. +The React SDK requires React 16.8.0 or above, since it uses **React Hooks API** introduced in that version. The SDK supports all major web browsers. It was built to support ES5 syntax but it depends on native support for ES6 `Promise`, `Map`, and `Set` objects, so these objects need to be **polyfilled** if they are not available in your target browsers, like IE 11. @@ -36,7 +36,7 @@ If you're looking for possible polyfill options, check [es6-promise](https://git ## Initialization -Set up Split in your code base in two steps. +Set up FME in your code base in two steps. ### 1. Import the SDK into your project @@ -55,15 +55,15 @@ yarn add @splitsoftware/splitio-react ```html - + ``` -### 2. Instantiate the SDK and create a new Split client +### 2. Instantiate the SDK and create a new SDK factory client -The code examples below show how to instantiate the SDK. The code creates a Split factory, which begins downloading your rollout plan so you can evaluate feature flags, and creates a client. +The code examples below show how to instantiate the SDK. The code creates a SDK factory, which begins downloading your rollout plan so you can evaluate feature flags, and creates a client. @@ -160,7 +160,7 @@ You can use it for more complex setups. For example, when you need to use the `S // If you were using the bundle via CDN, it'll be at `window.splitio.SplitFactory` import { SplitFactory, SplitFactoryProvider } from '@splitsoftware/splitio-react'; -// Create the Split factory object with your custom settings, using the re-exported function. +// Create the SDK factory object with your custom settings, using the re-exported function. const factory: SplitIO.IBrowserSDK = SplitFactory({ core: { authorizationKey: 'YOUR_CLIENT_SIDE_SDK_KEY', @@ -183,19 +183,19 @@ With the SDK package on NPM, you get the SplitIO namespace, which contains usefu Feel free to dive into the declaration files if IntelliSense is not enough! ::: -We recommend instantiating the Split factory once as a singleton and reusing it throughout your application. +We recommend instantiating the SDK factory once as a singleton and reusing it throughout your application. -Configure the SDK with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split UI, on your Admin settings page, API keys section. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. +Configure the SDK with the SDK key for the FME environment that you would like to access. The SDK key is available in Harness FME Admin settings. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. ## Using the SDK ### Get treatments with configurations -When the SDK is instantiated, it kicks off background tasks to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while its in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). +When the SDK is instantiated, it kicks off background tasks to update an in-memory cache with small amounts of data fetched from Harness servers. This process can take up to a few hundred milliseconds depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while its in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). -To make sure the SDK is properly loaded before asking it for a treatment, block until the SDK is ready, as shown below. We provide the `isReady` boolean prop based on the client that will be used by the component. Internally we listen for the `SDK_READY` event triggered by given SDK client to set the value of `isReady`. +To make sure the SDK is properly loaded before asking it for a treatment, block until the SDK is ready, as shown below. We provide the `isReady` boolean prop based on the client that will be used by the component. Internally we listen for the `SDK_READY` event triggered by given SDK factory client to set the value of `isReady`. -After the `isReady` prop is set to true, you can use the SDK. The `useSplitTreatments` hook returns the proper treatments based on the `names` prop value passed to it and the `core.key` value you passed in the config when instantiating the SDK. Then use the `treatments` property to access the treatment values as well as the corresponding [dynamic configurations](https://help.split.io/hc/en-us/articles/360026943552) that you defined in the Split user interface. Remember to handle the client returning control as a safeguard. +After the `isReady` prop is set to true, you can use the SDK. The `useSplitTreatments` hook returns the proper treatments based on the `names` prop value passed to it and the `core.key` value you passed in the config when instantiating the SDK. Then use the `treatments` property to access the treatment values as well as the corresponding [dynamic configurations](https://help.split.io/hc/en-us/articles/360026943552) that you defined in Harness FME. Remember to handle the client returning control as a safeguard. Similarly to the vanilla JS SDK, React SDK supports the ability to evaluate flags based on cached content when using [LOCALSTORAGE](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#configuration) as storage type. In this case, the `isReadyFromCache` prop will change to true almost instantly since access to the cache is synchronous, allowing you to consume flags earlier on components that are critical to your UI. Keep in mind that the data might be stale until `isReady` prop is true. Read more [below](#subscribe-to-events-and-changes). @@ -284,7 +284,7 @@ type TreatmentsWithConfig = { }; ``` -As you can see from the object structure, the config will be a stringified version of the configuration JSON defined in the Split user interface. If there is no configuration defined for a treatment, the SDK returns `null` for the config parameter. +As you can see from the object structure, the config will be a stringified version of the configuration JSON defined in Harness FME. If there is no configuration defined for a treatment, the SDK returns `null` for the config parameter. In some instances, you may want to evaluate treatments for multiple feature flags at once using flag sets. In that case, you can use the `useSplitTreatments` hook with the `flagSets` property rather than the `names` property. Like `names`, the `flagSets` property is an array of strings, each one corresponding to a different flag set name. @@ -359,7 +359,7 @@ export default class MyComponentToggle extends React.Component { ### Attribute syntax -To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK needs to be passed an attribute map at runtime. In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the underlying `getTreatmentsWithConfig` or `getTreatmentsWithConfigByFlagSets` call, whether you are evaluating using the `names` or `flagSets` property respectively. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in the Split user interface to decide whether to show the `on` or `off` treatment to this account. The SDK supports five types of attributes: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: +To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK needs to be passed an attribute map at runtime. In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the underlying `getTreatmentsWithConfig` or `getTreatmentsWithConfigByFlagSets` call, whether you are evaluating using the `names` or `flagSets` property respectively. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in Harness FME to decide whether to show the `on` or `off` treatment to this account. The SDK supports five types of attributes: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: * **Strings:** Use type String. * **Numbers:** Use type Number. @@ -583,7 +583,7 @@ If the `SplitFactoryProvider` component is created with a `config` prop, then th ## Track -Use the `client.track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Tracking events through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your features on your users' actions and metrics. +Use the `client.track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Tracking events through one of our SDKs or via the API is the first step to getting experimentation data into Harness FME and allows you to measure the impact of your features on your users' actions and metrics. [Learn more](https://help.split.io/hc/en-us/articles/360020585772) about using track events in feature flags. @@ -593,16 +593,16 @@ To track events, you must follow two steps: In the examples below, you can see that tracking events can take up to four arguments. The proper data type and syntax for each are: -* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined Harness FME. * **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: * Contains 63 characters or fewer. * Starts with a letter or number. * Contains only letters, numbers, hyphen, underscore, or period. * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` * **VALUE:** (Optional) The value to be used in creating the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is **Integer** or **Float**. -* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. +* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. FME currently supports three types of properties: strings, numbers, and booleans. -The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event to be sent back to Split's servers on the next event post. The SDK returns `false` if the current queue size is equal to the config set by `eventsQueueSize` config or if an incorrect input has been provided. +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event to be sent back to Harness servers on the next event post. The SDK returns `false` if the current queue size is equal to the config set by `eventsQueueSize` config or if an incorrect input has been provided. In case a bad input is provided, you can read more about our [SDK's expected behavior](https://help.split.io/hc/en-us/articles/360020585772-Track-events). @@ -678,7 +678,7 @@ The SDK has a number of knobs for configuring performance. Each knob is tuned to ## Localhost mode -For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To do this, start the Split SDK in **localhost** mode (also called off-the-grid mode). In this mode, the SDK neither polls or updates from Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the features. +For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To do this, start the SDK in **localhost** mode (also called off-the-grid mode). In this mode, the SDK neither polls or updates from Harness servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the features. When instantiating the SDK in localhost mode, your `authorizationKey` is `"localhost"`. Define the feature flags you want to use in the `features` object map. All `useSplitTreatments` calls for a feature flag return the treatment (and config, if defined) that you have defined in the map. You can then change the treatment as necessary for your testing. If you want to update a treatment or a config, or to add or remove feature flags from the mock cache, update the properties of the `features` object you've provided. The SDK simulates polling for changes and updates from the `features` object. Do not assign a new object to the `features` property because the SDK has a reference to the original object and will not detect the change. @@ -852,7 +852,7 @@ describe('MyApp', () => { ## Manager -Use the Split manager to get a list of features available to the Split client. To access the Manager in your code base, use the `useSplitManager` hook. +Use the Split manager to get a list of features available to the SDK factory client. To access the Manager in your code base, use the `useSplitManager` hook. @@ -888,7 +888,7 @@ To find all the details on the Manager available methods, see the [JavaScript SD ## Listener -Split SDKs send impression data back to Split servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression listener*. For that purpose, the SDK's configurations have a parameter called `impressionListener` where an implementation of `ImpressionListener` could be added. This implementation **must** define the `logImpression` method and it receives data in the following schema. +FME SDKs send impression data back to Harness servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression listener*. For that purpose, the SDK's configurations have a parameter called `impressionListener` where an implementation of `ImpressionListener` could be added. This implementation **must** define the `logImpression` method and it receives data in the following schema. | **Name** | **Type** | **Description** | | --- | --- | --- | @@ -980,9 +980,9 @@ This section describes advanced use cases and features provided by the SDK. ### Instantiate multiple SDK clients -Each JavaScript SDK client is tied to one specific customer id at a time which usually belongs to one traffic type (for example, `user`, `account`, `organization`). This enhances performance and reduces data cached within the SDK. +Each JavaScript SDK factory client is tied to one specific customer ID at a time which usually belongs to one traffic type (for example, `user`, `account`, `organization`). This enhances performance and reduces data cached within the SDK. -Split supports the ability to release based on multiple traffic types. With traffic types, you can release to `users` in one feature flag and `accounts` in another. If you are unfamiliar with using multiple traffic types, you can learn more [here](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type). +FME supports the ability to release based on multiple traffic types. With traffic types, you can release to `users` in one feature flag and `accounts` in another. If you are unfamiliar with using multiple traffic types, you can learn more [here](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type). If you need to roll out feature flags by different traffic types, instantiate multiple SDK clients, one for each traffic type. For example, you may want to roll out the feature `user-poll` by `users` and the feature `account-permissioning` by `accounts`. You can do this by retrieving different clients using the `useSplitClient` hook. @@ -1007,7 +1007,7 @@ const App = () => ( ); -// An example of how you can access a different SDK client to track events or evaluate flags in a function component with `useSplitClient` hook. +// An example of how you can access a different SDK factory client to track events or evaluate flags in a function component with `useSplitClient` hook. function MyComponentWithFlags(props) { // Calling useSplitClient with no `splitKey` parameter returns the client on the Split context. @@ -1102,11 +1102,11 @@ While the SDK does not put any limitations on the number of instances that can b The underlying JavaScript SDK has four different events: * `SDK_READY_FROM_CACHE`. This event fires once the SDK is ready to evaluate treatments using a version of your rollout plan cached in localStorage from a previous session (which might be stale). If there is data in localStorage, this event fires almost immediately, since access to localStorage is fast; otherwise, it doesn't fire. -* `SDK_READY`. This event fires once the SDK is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Split servers. -* `SDK_READY_TIMED_OUT`. This event fires if there is no cached version of your rollout plan cached in localStorage, and the SDK could not download the data from Split servers within the time specified by the `readyTimeout` configuration parameter. This event does not indicate that the SDK initialization was interrupted. The SDK continues downloading the rollout plan and fires the `SDK_READY` event when finished. This delayed `SDK_READY` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. -* `SDK_UPDATE`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in the Split user interface. +* `SDK_READY`. This event fires once the SDK is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Harness servers. +* `SDK_READY_TIMED_OUT`. This event fires if there is no cached version of your rollout plan cached in localStorage, and the SDK could not download the data from Harness servers within the time specified by the `readyTimeout` configuration parameter. This event does not indicate that the SDK initialization was interrupted. The SDK continues downloading the rollout plan and fires the `SDK_READY` event when finished. This delayed `SDK_READY` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. +* `SDK_UPDATE`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in Harness FME. -While you could potentially access the JavaScript SDK client from the Split context and track this yourself, this is not trivial. The `useSplitClient` and `useSplitTreatments` hooks accept four optional boolean parameters to trigger the re-render of the component. If the parameters are set to `true` (which is the default), the component re-renders when an `SDK_READY`, `SDK_READY_FROM_CACHE`, `SDK_UPDATE` or `SDK_READY_TIMED_OUT` event fires, and you can take action if you desire to do so. +While you could potentially access the JavaScript SDK factory client from the Split context and track this yourself, this is not trivial. The `useSplitClient` and `useSplitTreatments` hooks accept four optional boolean parameters to trigger the re-render of the component. If the parameters are set to `true` (which is the default), the component re-renders when an `SDK_READY`, `SDK_READY_FROM_CACHE`, `SDK_UPDATE` or `SDK_READY_TIMED_OUT` event fires, and you can take action if you desire to do so. * For `SDK_READY`, you can set the `updateOnSdkReady` parameter. * For `SDK_READY_FROM_CACHE`, you can set the `updateOnSdkReadyFromCache` parameter. @@ -1114,7 +1114,7 @@ While you could potentially access the JavaScript SDK client from the Split cont * For `SDK_UPDATE`, you can set the `updateOnSdkUpdate` parameter. The default value for all these parameters is `true`. -The `useSplitClient` and `useSplitTreatments` hooks return the SDK client and treatment evaluations respectively, together with a set of **status properties** to conditionally render the component. +The `useSplitClient` and `useSplitTreatments` hooks return the SDK factory client and treatment evaluations respectively, together with a set of **status properties** to conditionally render the component. These properties consist of the following: @@ -1392,7 +1392,7 @@ export const MyComponentWithFeatureFlags = (props) => { ### Usage with React Native SDK -The Split React SDK can be used in React Native applications by combining it with the React Native SDK. For that, you need to instantiate a factory with the React Native SDK and pass it to the React SDK `SplitFactoryProvider` component. The React SDK will use the factory to create the client and evaluate the feature flags. +The React SDK can be used in React Native applications by combining it with the React Native SDK. For that, you need to instantiate a factory with the React Native SDK and pass it to the React SDK `SplitFactoryProvider` component. The React SDK will use the factory to create the client and evaluate the feature flags. diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/redux-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/redux-sdk.md index d35e2ea2238..5ea067c69fa 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/redux-sdk.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-sdks/redux-sdk.md @@ -34,7 +34,7 @@ In SSR setups, our library code is prepared to run in Node.js 14+. ## Initialization -Set up Split in your code base in two steps. +Set up FME in your code base in two steps. ### 1. Import the SDK into your project @@ -83,7 +83,7 @@ const store = configureStore( splitio: splitReducer, ... // Combine Split reducer with your own reducers }), - // Split SDK requires thunk middleware, which is included by default by Redux Toolkit + // The SDK requires thunk middleware, which is included by default by Redux Toolkit ); // Initialize the SDK by calling the initSplitSdk and passing the config in the parameters object. @@ -114,7 +114,7 @@ const store = createStore( splitio: splitReducer, ... // Combine Split reducer with your own reducers }), - // Add thunk middleware, used by Split SDK async actions + // Add thunk middleware, used by SDK async actions applyMiddleware(thunk) ); @@ -136,7 +136,7 @@ const sdkNodeConfig = { } }; /** - * initSplitSdk should be called only once, to keep a single Split factory instance. + * initSplitSdk should be called only once, to keep a single SDK factory instance. * The returned action is dispatched each time a new store is created, to update * the Split status at the state. */ @@ -165,13 +165,13 @@ With the SDK package on NPM, you get the SplitIO namespace, which contains usefu Feel free to dive into the declaration files if IntelliSense is not enough! ::: -We recommend instantiating the Split factory once as a singleton and reusing it throughout your application. +We recommend instantiating the SDK factory once as a singleton and reusing it throughout your application. -Configure the SDK with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split UI, on your Admin settings page, API keys section. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. +Configure the SDK with the SDK key for the FME environment that you would like to access. The SDK key is available in Harness FME Admin settings. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. ## Using the SDK -The Split SDK via its reducer keeps a portion of the store state up to date. The Split state data adheres to the following schema: +The SDK via its reducer keeps a portion of the store state up to date. The state data adheres to the following schema: @@ -222,9 +222,9 @@ The Split SDK via its reducer keeps a portion of the store state up to date. The ### Basic use -When the SDK is initialized, it starts background tasks to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds depending on the size of data. If the SDK is asked to evaluate a feature flag while it's in this intermediate state, it may not have the necessary data and will queue the evaluation until it is ready. +When the SDK is initialized, it starts background tasks to update an in-memory cache with small amounts of data fetched from Harness servers. This process can take up to a few hundred milliseconds depending on the size of data. If the SDK is asked to evaluate a feature flag while it's in this intermediate state, it may not have the necessary data and will queue the evaluation until it is ready. -To make sure the SDK is fully loaded before using a treatment, wait until the SDK client is ready. You can check SDK readiness in one of the following ways: +To make sure the SDK is fully loaded before using a treatment, wait until the SDK factory client is ready. You can check SDK readiness in one of the following ways: - Provide an `onReady` callback as a parameter to the `initSplitSdk` function. - Check the `isReady` property of the `splitio` Redux state. @@ -322,7 +322,7 @@ store.dispatch(getTreatments({ splitNames: ['feature_flag_2', 'feature_flag_3'], -After feature flag treatments are part of the state, use the `splitio.treatments` slice of state or our selectors to access the feature flag evaluation results and write the code for the different treatments that you defined in the Split user interface. Remember to handle the client returning control in your code. +After feature flag treatments are part of the state, use the `splitio.treatments` slice of state or our selectors to access the feature flag evaluation results and write the code for the different treatments that you defined in Harness FME. Remember to handle the client returning control in your code. @@ -377,7 +377,7 @@ Note that these treatments won't be updated automatically when there is a change To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK's `getTreatments` action creator needs to be passed an attribute map at runtime. -In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatments` action creator call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in the Split user interface to decide whether to show the `on` or `off` treatment to this account. +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatments` action creator call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in Harness FME to decide whether to show the `on` or `off` treatment to this account. The SDK supports five types of attributes: strings, numbers, dates, booleans, and sets. The data type and syntax for each are: @@ -503,7 +503,7 @@ type TreatmentWithConfig = { -As you can see from the object structure, the config will be a stringified version of the configuration JSON defined in the Split user interface. If there is no configuration defined for a treatment, the SDK will return `null` for the config parameter. +As you can see from the object structure, the config will be a stringified version of the configuration JSON defined in Harness FME. If there is no configuration defined for a treatment, the SDK will return `null` for the config parameter. The `selectTreatmentWithConfigAndStatus` selector takes the exact same set of arguments as `selectTreatmentAndStatus`, as shown below. @@ -560,7 +560,7 @@ const treatmentResult = splitTreatments['key']['feature_flag_1']; ### Shutdown -Call the `destroySplitSdk` function to gracefully shutdown the Split SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. +Call the `destroySplitSdk` function to gracefully shutdown the SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. This function can be used as an action creator to update the `splitio` slice. @@ -605,21 +605,21 @@ Destroying the SDK is meant to be definitive. A call to the `destroySplitSdk` fu ## Track -You can use the `track` method to record any actions your users perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users’ actions and metrics. Go to [Events](https://help.split.io/hc/en-us/articles/360020585772) to learn more about using track events in feature flags. +You can use the `track` method to record any actions your users perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Harness FME and allows you to measure the impact of your feature flags on your users’ actions and metrics. Go to [Events](https://help.split.io/hc/en-us/articles/360020585772) to learn more about using track events in feature flags. The `track` method takes a params object with up to five arguments. The data type and syntax for each are: * **key:** The `key` variable used in the `getTreatments` call and firing this track event. The expected data type is **String**. -* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined Harness FME. * **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: * Contains 63 characters or fewer. * Starts with a letter or number. * Contains only letters, numbers, hyphen, underscore, or period. * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` * **VALUE:** (Optional) The value to be used in creating the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is **Integer** or **Float**. -* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. +* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. FME currently supports three types of properties: strings, numbers, and booleans. -The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event to be sent back to Split's servers on the next event post. The SDK returns `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method has been provided. +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event to be sent back to Harness servers on the next event post. The SDK returns `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method has been provided. It is important to mention that this method does not interact with the Redux store. It's only an abstraction on top of the underlying SDK track method, so you can only import one Split package. @@ -675,7 +675,7 @@ To learn about all the available configuration options, go to the [JavaScript SD ## Localhost mode -For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To do this, start the Split SDK in **localhost** mode (also called off-the-grid or offline mode). In this mode, the SDK neither polls or updates from Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the features. +For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To do this, start the SDK in **localhost** mode (also called off-the-grid or offline mode). In this mode, the SDK neither polls or updates from Harness servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the features. When instantiating the SDK in localhost mode, your `authorizationKey` is `"localhost"`. Define the feature flags you want to use in the `features` object map. All feature flag evaluations with `getTreatments` actions return the one treatment (and config, if defined) that you have defined in the map. You can then change the treatment as necessary for your testing. If you want to update a treatment or a config, or to add or remove feature flags from the mock cache, update the properties of the `features` object you've provided. The SDK simulates polling for changes and updates from it. Do not assign a new object to the `features` property because the SDK has a reference to the original object and will not detect the change. @@ -715,7 +715,7 @@ For a complete unit test example using Jest and React Testing Library, check [Ap ## Manager -Use the Split Manager to get a list of feature flags available to the Split client. The Manager uses the data fetched from Split servers upon SDK initialization, so you should wait for the SDK to be ready after initialization (as explained in the [basic use](#basic-use) section) before using the manager; otherwise, the manager functions will return null values and empty arrays. +Use the Split Manager to get a list of feature flags available to the SDK factory client. The Manager uses the data fetched from Harness servers upon SDK initialization, so you should wait for the SDK to be ready after initialization (as explained in the [basic use](#basic-use) section) before using the manager; otherwise, the manager functions will return null values and empty arrays. You can access the manager functionality through the exposed `getSplitNames`, `getSplit`, and `getSplits` helper functions, as explained below. @@ -807,7 +807,7 @@ For more details on about using the Manager, go to [JavaScript SDK Manager](http ## Listener -Split SDKs send impression data back to Split servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression listener*. For that purpose, the SDK's configurations have a parameter called `impressionListener` where an implementation of `ImpressionListener` could be added. This implementation **must** define the `logImpression` method and it receives data in the following schema. +FME SDKs send impression data back to Harness servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression listener*. For that purpose, the SDK's configurations have a parameter called `impressionListener` where an implementation of `ImpressionListener` could be added. This implementation **must** define the `logImpression` method and it receives data in the following schema. | **Name** | **Type** | **Description** | | --- | --- | --- | @@ -883,9 +883,9 @@ This section describes advanced use cases and features provided by the Redux SDK ### Instantiate multiple SDK clients -When running **on the client side** the Redux SDK client is tied to one specific key or ID at a time which usually belongs to one traffic type (for example, `user`, `account`, `organization`). This enhances performance and reduces caching data in the SDK. +When running **on the client side** the Redux SDK factory client is tied to one specific key or ID at a time which usually belongs to one traffic type (for example, `user`, `account`, `organization`). This enhances performance and reduces caching data in the SDK. -Split supports the ability to release features to multiple keys with different traffic types. With traffic types, you can release to `users` in one feature flag and `accounts` in another. Go to [Traffic type](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) to learn more. +FME supports the ability to release features to multiple keys with different traffic types. With traffic types, you can release to `users` in one feature flag and `accounts` in another. Go to [Traffic type](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) to learn more. If you need to roll out feature flags by different traffic types, the SDK instantiates multiple clients, one for each traffic type. For example, you may want to roll out the feature flag `user-poll` by `users` and the feature flag `account-permissioning` by `accounts`. @@ -937,9 +937,9 @@ While the SDK does not put any limitations on the number of instances that can b The underlying JavaScript SDK has four different events: * `SDK_READY_FROM_CACHE`. This event fires in client-side code if using the `LOCALSTORAGE` storage type. This event fires once the SDK is ready to evaluate treatments using the rollout plan cached in localStorage from a previous session (which might be stale). If there is data in localStorage, this event fires almost immediately, since access to localStorage is fast; otherwise, it doesn't fire. -* `SDK_READY`. This event fires once the SDK is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Split servers. -* `SDK_READY_TIMED_OUT`. This event fires if there is no cached version of your rollout plan in localStorage, and the SDK could not download the data from Split servers within the time specified by the `readyTimeout` configuration parameter. This event does not indicate that the SDK initialization was interrupted. The SDK continues downloading the rollout plan and fires the `SDK_READY` event when finished. This delayed `SDK_READY` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. -* `SDK_UPDATE`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in the Split user interface. +* `SDK_READY`. This event fires once the SDK is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Harness servers. +* `SDK_READY_TIMED_OUT`. This event fires if there is no cached version of your rollout plan in localStorage, and the SDK could not download the data from Harness servers within the time specified by the `readyTimeout` configuration parameter. This event does not indicate that the SDK initialization was interrupted. The SDK continues downloading the rollout plan and fires the `SDK_READY` event when finished. This delayed `SDK_READY` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. +* `SDK_UPDATE`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in Harness FME. Besides managing `SDK_READY` on initialization, as explained in the [basic use](#basic-use) section, you can also add callbacks for the other events as shown below: @@ -977,14 +977,14 @@ store.dispatch(initSplitSdk({ -You can also access the readiness state of any SDK client with the `selectStatus` selector, or when retrieving treatments with the `selectTreatmentAndStatus` or `selectTreatmentWithConfigAndStatus` selectors. +You can also access the readiness state of any SDK factory client with the `selectStatus` selector, or when retrieving treatments with the `selectTreatmentAndStatus` or `selectTreatmentWithConfigAndStatus` selectors. ```javascript import { selectStatus, selectTreatmentAndStatus } from '@splitsoftware/splitio-redux'; -// Retrieves current status of the SDK client with USER_ID key. If no key is provided, the main client status is returned. +// Retrieves current status of the SDK factory client with USER_ID key. If no key is provided, the main client status is returned. const { isReady, isReadyFromCache, isTimedout, hasTimedout, isDestroyed, lastUpdate } = selectStatus(store.getState().splitio, USER_ID); // Readiness properties are also available in the selector result. diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/_category_.json b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/_category_.json index 898691916d5..536ad9c8bc6 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/_category_.json +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/_category_.json @@ -1,5 +1,5 @@ { - "label": "Client-side Suites", + "label": "Client-side SDK Suites", "collapsible": "true", "collapsed": "true", "className": "red", diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/android-suite.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/android-suite.md index 2cfa7b43cd6..2854a105e5a 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/android-suite.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/android-suite.md @@ -12,9 +12,9 @@ helpdocs_is_published: true import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -This guide provides detailed information about our Android Suite, an SDK designed to harness the full power of Split. The Android Suite is built on top of the [Android SDK](https://help.split.io/hc/en-us/articles/360020343291-Android-SDK) and the [Android RUM Agent](https://help.split.io/hc/en-us/articles/18530305949837-Android-RUM-Agent), offering a unified solution, optimized for Android development. +This guide provides detailed information about our Android Suite, an SDK designed to leverage the full power of FME. The Android Suite is built on top of the [Android SDK](https://help.split.io/hc/en-us/articles/360020343291-Android-SDK) and the [Android RUM Agent](https://help.split.io/hc/en-us/articles/18530305949837-Android-RUM-Agent), offering a unified solution, optimized for Android development. -The Suite provides the all-encompassing essential programming interface for working with your Split feature flags, as well as capabilities for automatically tracking performance measurements and user events. Code currently using Android SDK or Android RUM Agent can be easily upgraded to Android Suite, which is designed as a drop-in replacement. +The Suite provides the all-encompassing essential programming interface for working with your FME feature flags, as well as capabilities for automatically tracking performance measurements and user events. Code currently using Android SDK or Android RUM Agent can be easily upgraded to Android Suite, which is designed as a drop-in replacement. ## Language support @@ -22,7 +22,7 @@ This library is designed for Android applications written in Java or Kotlin and ## Initialization -Set up Split in your code base with the following two steps: +Set up FME in your code base with the following two steps: ### 1. Import the Suite into your project @@ -36,14 +36,14 @@ implementation 'io.split.client:android-suite:2.0.0' When upgrading from Split's Android SDK and/or Android RUM Agent to Android Suite, you need to remove individual project dependencies for the SDK and Agent. The dependency for the Suite replaces these dependencies. ::: -### 2. Instantiate the Suite and create a new Split client +### 2. Instantiate the Suite and create a new SDK client In your code, instantiate the Suite client as shown below. ```java -// Split SDK key +// SDK key String sdkKey = "YOUR_SDK_KEY"; // Build Suite configuration by default @@ -64,7 +64,7 @@ SplitClient client = suite.client(); ```kotlin -// Split SDK key +// SDK key val sdkKey = "YOUR_SDK_KEY" // Build Suite configuration by default @@ -87,12 +87,12 @@ val client: SplitClient = splitFactory.client() :::warning[Important] -If you are upgrading from Split's Android RUM Agent to Android Suite and you have setup or config information for the Android RUM Agent in the `AndroidManifest.xml`, then this information will be overridden by the Suite initialization. That is why we recommended that you remove this information from `AndroidManifest.xml` when upgrading. +If you are upgrading from FME's Android RUM Agent to Android Suite and you have setup or config information for the Android RUM Agent in the `AndroidManifest.xml`, then this information will be overridden by the Suite initialization. That is why we recommended that you remove this information from `AndroidManifest.xml` when upgrading. ::: -When the Suite is instantiated, it starts synchronizing feature flag and segment definitions from Split servers, and also starts collecting performance and user events for the configured key and its optional traffic type (which if not set, defaults to `'user'`). +When the Suite is instantiated, it starts synchronizing feature flag and segment definitions from Harness servers, and also starts collecting performance and user events for the configured key and its optional traffic type (which if not set, defaults to `'user'`). -We recommend instantiating the Split factory once as a singleton and reusing it throughout your application. +We recommend instantiating the SDK factory once as a singleton and reusing it throughout your application. Configure the Suite with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split UI, on your Admin settings page, API keys section. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. @@ -100,7 +100,7 @@ Configure the Suite with the SDK key for the Split environment that you would li ### Basic use -When the Suite is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds depending on the size of the data. If the Suite is asked to evaluate which treatment to show to a user for a specific feature flag while in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the Suite does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). +When the Suite is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Harness servers. This process can take up to a few hundred milliseconds depending on the size of the data. If the Suite is asked to evaluate which treatment to show to a user for a specific feature flag while in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the Suite does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). To make sure the Suite is properly loaded before asking it for a treatment, block until the Suite is ready, as shown below. We set the client to listen for the `SDK_READY` event triggered by the Suite before asking for an evaluation. @@ -286,7 +286,7 @@ val result = client.clearAttributes() ### Multiple evaluations at once -In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` from the Split client to do this. +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` from the SDK client to do this. * `getTreatments`: Pass a list of the feature flag names you want treatments for. * `getTreatmentsByFlagSet`: Evaluate all flags that are part of the provided set name and are cached on the Suite instance. * `getTreatmentsByFlagSets`: Evaluate all flags that are part of the provided set names and are cached on the Suite instance. @@ -460,7 +460,7 @@ The `client.track()` method sends events **_for the identity configured on the c * Contains only letters, numbers, hyphen, underscore, or period. * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` * **VALUE:** (Optional) The value to be used in creating the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is **Integer** or **Float**. -* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. +* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. FME currently supports three types of properties: strings, numbers, and booleans. The `suite.track()` method sends events **_for all the identities_** configured on all instances of the Suite clients. For those clients that have not been configured with a traffic type, this `track` method uses the default traffic type `user`. This `track` method can take up to three of the four arguments described above: `EVENT_TYPE`, `VALUE`, and `PROPERTIES`. @@ -563,7 +563,7 @@ suite.track("screen_load_time", null, properties); -The `client.track()` methods return a boolean value of `true` or `false` to indicate whether or not the Suite was able to successfully queue the event, to be sent back to Split's servers on the next event post. +The `client.track()` methods return a boolean value of `true` or `false` to indicate whether or not the Suite was able to successfully queue the event, to be sent back to Harness servers on the next event post. ### Shutdown @@ -597,18 +597,18 @@ Feature flagging parameters: | **Configuration** | **Description** | **Default value** | | --- | --- | --- | -| featuresRefreshRate | The Suite polls Split servers for changes to feature flags at this rate (in seconds). | 3600 seconds | -| segmentsRefreshRate | The Suite polls Split servers for changes to segments at this rate (in seconds). | 1800 seconds | -| impressionsRefreshRate | The treatment log captures which customer saw what treatment (on, off, etc.) at what time. This log is periodically flushed back to Split servers. This configuration controls how quickly the cache expires after a write (in seconds). | 1800 seconds | -| telemetryRefreshRate | The Suite caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers (in seconds). | 3600 seconds (1 hour) | +| featuresRefreshRate | The Suite polls Harness servers for changes to feature flags at this rate (in seconds). | 3600 seconds | +| segmentsRefreshRate | The Suite polls Harness servers for changes to segments at this rate (in seconds). | 1800 seconds | +| impressionsRefreshRate | The treatment log captures which customer saw what treatment (on, off, etc.) at what time. This log is periodically flushed back to Harness servers. This configuration controls how quickly the cache expires after a write (in seconds). | 1800 seconds | +| telemetryRefreshRate | The Suite caches diagnostic data that it periodically sends to Harness servers. This configuration controls how frequently this data is sent back to Harness servers (in seconds). | 3600 seconds (1 hour) | | eventsQueueSize | When using the `track` method, the number of **events** to be kept in memory. | 10000 | -| eventFlushInterval | When using the `track` method, how often is the events queue flushed to Split's servers. | 1800 seconds | +| eventFlushInterval | When using the `track` method, how often is the events queue flushed to Harness servers. | 1800 seconds | | eventsPerPush | Maximum size of the batch to push events. | 2000 | | trafficType | When using the `track` method, the default traffic type to be used. | not set | | connectionTimeout | HTTP client connection timeout (in ms). | 10000 ms | | readTimeout | HTTP socket read timeout (in ms). | 10000 ms | | impressionsQueueSize | Default queue size for impressions. | 30K | -| disableLabels | Disable labels from being sent to Split backend. Labels may contain sensitive information. | true | +| disableLabels | Disable labels from being sent to Harness servers. Labels may contain sensitive information. | true | | proxyHost | The location of the proxy using standard URI: `scheme://user:password@domain:port/path`. If no port is provided, the Suite defaults to port 80. | null | | ready | Maximum amount of time in milliseconds to wait before notifying a timeout. | -1 (not set) | | synchronizeInBackground | Activates synchronization when application host is in background. | false | @@ -619,7 +619,7 @@ Feature flagging parameters: | syncConfig | Optional SyncConfig instance. Use it to filter specific feature flags to be synced and evaluated by the Suite. These filters can be created with the `SplitFilter::bySet` static function (recommended, flag sets are available in all tiers), or `SplitFilter::byName` static function, and appended to this config using the `SyncConfig` builder. If not set or empty, all feature flags are downloaded by the Suite. | null | | persistentAttributesEnabled | Enables saving attributes on persistent cache which is loaded as part of the SDK_READY_FROM_CACHE flow. All functions that mutate the stored attributes map affect the persistent cache. | false | | syncEnabled | Controls the SDK continuous synchronization flags. When `true`, a running Suite processes rollout plan updates performed in the Split user interface (default). When `false`, it fetches all data upon init, which ensures a consistent experience during a user session and optimizes resources when these updates are not consumed by the app. | true | -| impressionsMode | This configuration defines how impressions (decisioning events) are queued on the Suite. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Split; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, ALL impressions are queued and sent to Split; this is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. | `OPTIMIZED` | +| impressionsMode | This configuration defines how impressions (decisioning events) are queued on the Suite. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Harness; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, ALL impressions are queued and sent to Harness; this is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. | `OPTIMIZED` | | userConsent | User consent status used to control the tracking of events and impressions. Possible values are `GRANTED`, `DECLINED`, and `UNKNOWN`. See [User consent](#user-consent) for details. | `GRANTED` | | encryptionEnabled | If set to `true`, the local database contents is encrypted. | false | | prefix | If set, the prefix will be prepended to the database name used by the Suite. | null | @@ -629,7 +629,7 @@ Suite RUM agent parameters: | **Configuration** | **Description** | **Default value** | | --- | --- | --- | -| prefix | Optional prefix to append to the `eventTypeId` of the events sent to Split by the Suite RUM agent. For example, if you set the prefix to 'my-app', the event type 'error' will be sent as 'my-app.error'. | null | +| prefix | Optional prefix to append to the `eventTypeId` of the events sent to Harness by the Suite RUM agent. For example, if you set the prefix to 'my-app', the event type 'error' will be sent as 'my-app.error'. | null | Shared parameters: @@ -682,7 +682,7 @@ val suite: SplitSuite = SplitSuiteBuilder.build( ## Localhost mode -For testing, a developer can put code behind feature flags on their development machine without the Suite requiring network connectivity. To achieve this, you can start the Suite in **localhost** mode (aka, off-the-grid mode). In this mode, the Suite neither polls or updates Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the feature flags. To use the Suite in localhost mode, replace the SDK Key with `localhost`, as shown in the example below/ +For testing, a developer can put code behind feature flags on their development machine without the Suite requiring network connectivity. To achieve this, you can start the Suite in **localhost** mode (aka, off-the-grid mode). In this mode, the Suite neither polls or updates Harness servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the feature flags. To use the Suite in localhost mode, replace the SDK Key with `localhost`, as shown in the example below/ The format for defining the definitions is as follows: @@ -752,7 +752,7 @@ val client = SplitSuiteBuilder.build( ## Manager -Use the Split Manager to get a list of feature flags available to the Split client. To instantiate a Manager in your code base, use the same factory that you used for your client. +Use the Split Manager to get a list of feature flags available to the SDK client. To instantiate a Manager in your code base, use the same factory that you used for your client. @@ -863,7 +863,7 @@ class SplitView( ## Listener -The Split Suite sends impression data back to Split servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression listener*. +The Split Suite sends impression data back to Harness servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression listener*. The Suite sends the generated impressions to the impression listener right away. Because of this, be careful while implementing handling logic to avoid blocking the thread. Generally speaking, you should create a separate thread to handle incoming impressions. Refer to the snippet below: @@ -958,7 +958,7 @@ In regards with the data available here, refer to the `Impression` objects inter ## Flush -The `flush` method sends the data stored in memory (impressions and events tracked using client's `track` method) to the Split cloud and clears the successfully posted data. If a connection issue is experienced, the data is sent on the next attempt. If you want to flush all pending data when your app goes to the background, a good place to call this method is the `onPause` callback of your activity. +The `flush` method sends the data stored in memory (impressions and events tracked using client's `track` method) to the Harness FME servers and clears the successfully posted data. If a connection issue is experienced, the data is sent on the next attempt. If you want to flush all pending data when your app goes to the background, a good place to call this method is the `onPause` callback of your activity. @@ -993,7 +993,7 @@ This section describes advanced use cases and features provided by the Suite. ### Instantiate multiple clients -Split supports the ability to create multiple clients, one for each user ID. Each Suite client is tied to one specific customer ID at a time. For example, if you need to roll out feature flags for different user IDs, you can instantiate multiple clients, one for each ID. You can then evaluate them using the corresponding client. +FME supports the ability to create multiple clients, one for each user ID. Each Suite client is tied to one specific customer ID at a time. For example, if you need to roll out feature flags for different user IDs, you can instantiate multiple clients, one for each ID. You can then evaluate them using the corresponding client. You can do this with the example below: @@ -1054,7 +1054,7 @@ userClient.on(SplitEvent.SDK_READY, object : SplitEventTask() { -The events captured by the Suite's RUM agent are sent to Split servers using the traffic types and keys of the created client. If no traffic type is provided, the traffic type is `user` by default. +The events captured by the Suite's RUM agent are sent to Harness servers using the traffic types and keys of the created client. If no traffic type is provided, the traffic type is `user` by default. :::info[Number of Suite instances] While the Suite does not put any limitations on the number of instances that can be created, we strongly recommend keeping the number of instances down to **one** or **two**. @@ -1065,8 +1065,8 @@ While the Suite does not put any limitations on the number of instances that can You can listen for four different events from the Suite. * `SDK_READY_FROM_CACHE`. This event fires once the Suite is ready to evaluate treatments using a locally cached version of your rollout plan from a previous session (which might be stale). If there is data in the cache, this event fires almost immediately, since access to the cache is fast; otherwise, it doesn't fire. -* `SDK_READY`. This event fires once the Suite is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Split servers. -* `SDK_READY_TIMED_OUT`. This event fires if there is no cached version of your rollout plan in disk cache, and the Suite could not download the data from Split servers within the time specified by the `ready` setting of the `SplitClientConfig` object. This event does not indicate that the Suite initialization was interrupted. The Suite continues downloading the rollout plan and fires the `SDK_READY` event when finished. This delayed `SDK_READY` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. +* `SDK_READY`. This event fires once the Suite is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Harness servers. +* `SDK_READY_TIMED_OUT`. This event fires if there is no cached version of your rollout plan in disk cache, and the Suite could not download the data from Harness servers within the time specified by the `ready` setting of the `SplitClientConfig` object. This event does not indicate that the Suite initialization was interrupted. The Suite continues downloading the rollout plan and fires the `SDK_READY` event when finished. This delayed `SDK_READY` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. * `SDK_UPDATE`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in the Split user interface. To define what is executed after each event, create an extension of `SplitEventTask`. @@ -1190,8 +1190,8 @@ The Suite allows you to disable the tracking of events and impressions until use The `userConsent` configuration parameter lets you set the initial consent status of the Suite instance, and the Suite method `UserConsent.setStatus(boolean)` lets you grant (enable) or decline (disable) the dynamic data tracking. There are three possible initial states: - * `'GRANTED'`: the user grants consent for tracking events and impressions. The Suite sends them to Split cloud. This is the default value if `userConsent` param is not defined. - * `'DECLINED'`: the user declines consent for tracking events and impressions. The Suite does not send them to Split cloud. + * `'GRANTED'`: the user grants consent for tracking events and impressions. The Suite sends them to Harness FME servers. This is the default value if `userConsent` param is not defined. + * `'DECLINED'`: the user declines consent for tracking events and impressions. The Suite does not send them to Harness FME servers. * `'UNKNOWN'`: the user neither grants nor declines consent for tracking events and impressions. The Suite tracks them in its internal storage, and eventually either sends them or not if the consent status is updated to `'GRANTED'` or `'DECLINED'` respectively. The status can be updated at any time with the `setUserConsent` Suite method. @@ -1199,16 +1199,16 @@ There are three possible initial states: ```java // Overwrites the initial consent status of the Suite instance, which is 'GRANTED' by default. // 'UNKNOWN' status represents that the user has neither granted nor declined consent for tracking data, -// so the Suite locally tracks data but not send it to Split cloud until consent is changed to 'GRANTED'. +// so the Suite locally tracks data but not send it to Harness FME servers until consent is changed to 'GRANTED'. SplitClientConfig config = SplitClientConfig.builder() .userConsent(UserConsent.UNKNOWN) .build(); SplitSuite suite = SplitSuiteBuilder.build("YOUR_SDK_KEY", new Key(mUserKey, null), config, context); -// Changed User Consent status to 'GRANTED'. Data is sent to Split cloud. +// Changed User Consent status to 'GRANTED'. Data is sent to Harness FME servers. suite.setUserConsent(true); -// Changed User Consent status to 'DECLINED'. Data is not sent to Split cloud. +// Changed User Consent status to 'DECLINED'. Data is not sent to Harness FME servers. suite.setUserConsent(false); // The 'getUserConsent' method returns User Consent status. // We expose the constants for customer checks and tracking. @@ -1225,16 +1225,16 @@ if (suite.getUserConsent() == UserConsent.DECLINED) { ```kotlin // Overwrites the initial consent status of the Suite instance, which is 'GRANTED' by default. // 'UNKNOWN' status represents that the user has neither granted nor declined consent for tracking data, -// so the Suite locally tracks data but not send it to Split cloud until consent is changed to 'GRANTED'. +// so the Suite locally tracks data but not send it to Harness FME servers until consent is changed to 'GRANTED'. val config: SplitClientConfig = SplitClientConfig.builder() .userConsent(UserConsent.UNKNOWN) .build() val suite: SplitSuite = SplitSuiteBuilder.build("YOUR_SDK_KEY", Key(mUserKey, null), config, context) -// Changed User Consent status to 'GRANTED'. Data is sent to Split cloud. +// Changed User Consent status to 'GRANTED'. Data is sent to Harness FME servers. suite.setUserConsent(true) -// Changed User Consent status to 'DECLINED'. Data is not sent to Split cloud. +// Changed User Consent status to 'DECLINED'. Data is not sent to Harness FME servers. suite.setUserConsent(false) // The 'getUserConsent' method returns User Consent status. // We expose the constants for customer checks and tracking. @@ -1285,7 +1285,7 @@ CertificatePinningConfiguration certPinningConfig = CertificatePinningConfigurat .build(); -// Set the CertificatePinningConfiguration property for the Split client configuration +// Set the CertificatePinningConfiguration property for the SDK client configuration SplitClientConfig config = SplitClientConfig.builder() .certificatePinningConfiguration(certPinningConfig) // you can add other configuration properties here @@ -1315,7 +1315,7 @@ val certPinningConfig = CertificatePinningConfiguration.builder() .build() -// Set the CertificatePinningConfiguration property for the Split client configuration +// Set the CertificatePinningConfiguration property for the SDK client configuration val config = SplitClientConfig.builder() .certificatePinningConfiguration(certPinningConfig) // you can add other configuration properties here diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/browser-suite.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/browser-suite.md index 5068ec7fc52..7e8b4c759b2 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/browser-suite.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/browser-suite.md @@ -12,9 +12,9 @@ helpdocs_is_published: true import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -This guide provides detailed information about our JavaScript Browser Suite, an SDK designed to harness the full power of Split. The Browser Suite is built on top of the [Browser SDK](https://help.split.io/hc/en-us/articles/360058730852-Browser-SDK) and the [Browser RUM Agent](https://help.split.io/hc/en-us/articles/360030898431-Browser-RUM-agent), offering a unified solution, optimized for web development. +This guide provides detailed information about our JavaScript Browser Suite, an SDK designed to leverage the full power of FME. The Browser Suite is built on top of the [Browser SDK](https://help.split.io/hc/en-us/articles/360058730852-Browser-SDK) and the [Browser RUM Agent](https://help.split.io/hc/en-us/articles/360030898431-Browser-RUM-agent), offering a unified solution, optimized for web development. -The Suite provides the all-encompassing essential programming interface for working with your Split feature flags, as well as capabilities for automatically tracking performance measurements and user events. Code currently using Browser SDK or Browser RUM Agent can be easily upgraded to Browser Suite, which is designed as a drop-in replacement. +The Suite provides the all-encompassing essential programming interface for working with your FME feature flags, as well as capabilities for automatically tracking performance measurements and user events. Code currently using Browser SDK or Browser RUM Agent can be easily upgraded to Browser Suite, which is designed as a drop-in replacement. ## Language support @@ -22,7 +22,7 @@ The JavaScript Browser Suite supports all major browsers. While the library was ## Initialization -Set up Split in your code base with the following two steps: +Set up FME in your code base with the following two steps: ### 1. Import the Suite into your project @@ -52,7 +52,7 @@ We strongly recommend installing the SDK via NPM or your package manager of choi We also support a prebuilt bundle distributed via CDN. This is particularly useful for quick tests, PoC's or very specific use cases, but it is a large file and may slow down your page load time. ::: -### 2. Instantiate the Suite and create a new Split client +### 2. Instantiate the Suite and create a new SDK client In your code, instantiate the Suite client as shown below. @@ -116,7 +116,7 @@ var client = suite.client(); -When the Suite is instantiated, it starts synchronizing feature flag and segment definitions from Split servers, and also starts collecting performance and user events for the configured key and its optional traffic type (which if not set, defaults to `'user'`). +When the Suite is instantiated, it starts synchronizing feature flag and segment definitions from Harness servers, and also starts collecting performance and user events for the configured key and its optional traffic type (which if not set, defaults to `'user'`). We recommend instantiating the Suite once as a singleton and reusing it throughout your application. @@ -126,7 +126,7 @@ Configure the Suite with the SDK key for the Split environment that you would li ### Basic use -When the Suite is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds depending on the size of the data. If the Suite is asked to evaluate which treatment to show to a user for a specific feature flag while in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the Suite does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). +When the Suite is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Harness servers. This process can take up to a few hundred milliseconds depending on the size of the data. If the Suite is asked to evaluate which treatment to show to a user for a specific feature flag while in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the Suite does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). To make sure the Suite is properly loaded before asking it for a treatment, block until the Suite is ready, as shown below. We set the client to listen for the `SDK_READY` event triggered by the Suite before asking for an evaluation. @@ -288,7 +288,7 @@ var result = client.clearAttributes(); ### Multiple evaluations at once -In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` from the Split client to do this. +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` from the SDK client to do this. * `getTreatments`: Pass a list of the feature flag names you want treatments for. * `getTreatmentsByFlagSet`: Evaluate all flags that are part of the provided set name and are cached on the Suite instance. * `getTreatmentsByFlagSets`: Evaluate all flags that are part of the provided set names and are cached on the Suite instance. @@ -467,7 +467,7 @@ The Suite client's `track` method sends events for the identity configured on th * Contains only letters, numbers, hyphen, underscore, or period. * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` * **VALUE:** (Optional) The value to be used in creating the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is **Integer** or **Float**. -* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. +* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. FME currently supports three types of properties: strings, numbers, and booleans. The Suite RUM agent's `track` method sends events for all the identities configured on all instances of the Suite clients. For those clients that have not been configured with a traffic type, the `track` method uses the default traffic type `user`. The Suite RUM agent's `track` method can take up to three of the four arguments described above: `EVENT_TYPE`, `VALUE`, and `PROPERTIES`. @@ -506,7 +506,7 @@ var queued = SplitRumAgent.track('page_load_time', null, properties); -The `track` methods returns a boolean value of `true` or `false` to indicate whether or not the Suite was able to successfully queue the event to be sent back to Split's servers on the next event post. The `track` method returns `false` if the current queue size is equal to the config set by `eventsQueueSize` or if incorrect input has been provided. See the [Track events](https://help.split.io/hc/en-us/articles/360020585772-Track-events) documentation for more information. +The `track` methods returns a boolean value of `true` or `false` to indicate whether or not the Suite was able to successfully queue the event to be sent back to Harness servers on the next event post. The `track` method returns `false` if the current queue size is equal to the config set by `eventsQueueSize` or if incorrect input has been provided. See the [Track events](https://help.split.io/hc/en-us/articles/360020585772-Track-events) documentation for more information. ### Shutdown @@ -543,20 +543,20 @@ Feature flagging parameters: | **Configuration** | **Description** | **Default value** | | --- | --- | --- | -| core.labelsEnabled | Enable impression labels from being sent to Split cloud. Labels may contain sensitive information. | true | +| core.labelsEnabled | Enable impression labels from being sent to Harness FME servers. Labels may contain sensitive information. | true | | startup.readyTimeout | Maximum amount of time in seconds to wait before firing the `SDK_READY_TIMED_OUT` event | 10 | | startup.requestTimeoutBeforeReady | The Suite has two main endpoints it uses /splitChanges and /mySegments that it hits to get ready. This config sets how long (in seconds) the Suite will wait for each request it makes as part of getting ready. | 5 | | startup.retriesOnFailureBeforeReady | How many retries on /splitChanges and /mySegments we will do while getting the Suite ready | 1 | | startup.eventsFirstPushWindow | Use to set a specific timer (expressed in seconds) for the first push of events, starting on Suite initialization. | 10 | -| scheduler.featuresRefreshRate | The Suite polls Split servers for changes to feature rollout plans. This parameter controls this polling period in seconds. | 60 | -| scheduler.segmentsRefreshRate | The Suite polls Split servers for changes to segment definitions. This parameter controls this polling period in seconds. | 60 | -| scheduler.impressionsRefreshRate | The Suite sends information on who got what treatment at what time back to Split servers to power analytics. This parameter controls how often this data is sent to Split servers. The parameter should be in seconds. | 300 | +| scheduler.featuresRefreshRate | The Suite polls Harness servers for changes to feature rollout plans. This parameter controls this polling period in seconds. | 60 | +| scheduler.segmentsRefreshRate | The Suite polls Harness servers for changes to segment definitions. This parameter controls this polling period in seconds. | 60 | +| scheduler.impressionsRefreshRate | The Suite sends information on who got what treatment at what time back to Harness servers to power analytics. This parameter controls how often this data is sent to Harness servers. The parameter should be in seconds. | 300 | | scheduler.impressionsQueueSize | The max amount of impressions we queue. If the queue is full, the Suite flushes the impressions and resets the timer. | 30000 | -| scheduler.eventsPushRate | The Suite sends tracked events to Split servers. This setting controls that flushing rate in seconds. | 60 | +| scheduler.eventsPushRate | The Suite sends tracked events to Harness servers. This setting controls that flushing rate in seconds. | 60 | | scheduler.eventsQueueSize | The max amount of events we queue. If the queue is full, the Suite flushes the events and resets the timer. | 500 | -| scheduler.telemetryRefreshRate | The Suite caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers (in seconds). | 3600 seconds (1 hour) | +| scheduler.telemetryRefreshRate | The Suite caches diagnostic data that it periodically sends to Harness servers. This configuration controls how frequently this data is sent back to Harness servers (in seconds). | 3600 seconds (1 hour) | | sync.splitFilters | Filter specific feature flags to be synced and evaluated by the Suite. This is formed by a type string property and a list of string values for the given criteria. Using the types 'bySet' (recommended, flag sets are available in all tiers) or 'byName', pass an array of strings defining the query. If empty or unset, all feature flags are downloaded by the Suite. | [] | -| sync.impressionsMode | This configuration defines how impressions (decisioning events) are queued on the Suite. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Split; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, ALL impressions are queued and sent to Split; this is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. | `OPTIMIZED` | +| sync.impressionsMode | This configuration defines how impressions (decisioning events) are queued on the Suite. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Harness; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, ALL impressions are queued and sent to Harness; this is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. | `OPTIMIZED` | | sync.enabled | Controls the Suite continuous synchronization flags. When `true`, a running Suite processes rollout plan updates performed in the Split user interface (default). When `false`, it fetches all data upon init, which ensures a consistent experience during a user session and optimizes resources when these updates are not consumed by the app. | true | | sync.requestOptions.getHeaderOverrides | A callback function that can be used to override the Authentication header or append new headers to the Suite's HTTP(S) requests. | undefined | | storage | Pluggable storage instance to be used by the Suite as a complement to in memory storage. Only supported option today is `InLocalStorage`. See the [Configuration](#configuring-localstorage-cache-for-the-sdk) section for details. | In memory storage | @@ -566,7 +566,7 @@ Suite RUM agent parameters: | **Configuration** | **Description** | **Default value** | | --- | --- | --- | -| rumAgent.prefix | Optional prefix to append to the `eventTypeId` of the events sent to Split by the RUM Agent. For example, if you set the prefix to 'my-app', the event type 'error' will be sent as 'my-app.error'. | undefined | +| rumAgent.prefix | Optional prefix to append to the `eventTypeId` of the events sent to Harness by the RUM Agent. For example, if you set the prefix to 'my-app', the event type 'error' will be sent as 'my-app.error'. | undefined | | rumAgent.pushRate | The Agent posts the queued events data in bulks. This parameter controls the posting rate in seconds. | 30 | | rumAgent.queueSize | The maximum number of event items the RUM Agent will queue. If more values are queued, events will be dropped until they are sent to Split. | 5000 | | rumAgent.eventCollectors | The RUM Agent tracks some events by default using event collectors. These event collectors include errors, navigation timing metrics (`page.load.time` and `time.to.dom.interactive` event types), and Web-Vitals. You can disable any of them by setting their value to `false`. Go to [RUM Agent events](https://help.split.io/hc/en-us/articles/360030898431-Browser-RUM-Agent#events) for more information on each event. | \{ errors: true, navigationTiming: true, webVitals: true \} | @@ -700,7 +700,7 @@ const client = suite.client(); ## Localhost mode -For testing, a developer can evaluate Split feature flags on their development machine without requiring network connectivity. To achieve this, the Suite can be started in **localhost** mode (aka off-the-grid or offline mode). In this mode, the Suite neither polls nor updates Split servers. Instead, it uses an in-memory data structure to determine the treatment returned by any given feature flag. +For testing, a developer can evaluate FME feature flags on their development machine without requiring network connectivity. To achieve this, the Suite can be started in **localhost** mode (aka off-the-grid or offline mode). In this mode, the Suite neither polls nor updates Harness servers. Instead, it uses an in-memory data structure to determine the treatment returned by any given feature flag. Define the feature flags you want to use in the `features` object map. All `getTreatment` calls for a feature flag now only return the one treatment (and config, if defined) that you have defined in the map. You can then change the treatment as necessary for your testing. To update a treatment or a config, or to add or remove feature flags from the mock cache, update the properties of the `features` object you've provided. The SDK simulates polling for changes and updates from it. Do not assign a new object to the `features` property because the SDK has a reference to the original object and will not detect the change. @@ -753,7 +753,7 @@ client.on(client.Event.SDK_READY, () => { ## Manager -Use the Split Manager to get a list of features available to the Split client. To instantiate a Manager in your code base, use the same suite instance that you used for your client: +Use the Split Manager to get a list of features available to the SDK client. To instantiate a Manager in your code base, use the same suite instance that you used for your client: @@ -762,11 +762,6 @@ Use the Split Manager to get a list of features available to the Split client. T var suite = SplitSuite({ core: { authorizationKey: 'YOUR_SDK_KEY', - // the key can be the logged in - // user id, or the account id that - // the logged in user belongs to. - // The type of customer (user, account, custom) - // is chosen during Split's sign-up process. key: 'key' } }); @@ -783,11 +778,6 @@ manager.once(manager.Event.SDK_READY, function() { const suite: ISuiteSDK = SplitSuite({ core: { authorizationKey: 'YOUR_SDK_KEY', - // the key can be the logged in - // user id, or the account id that - // the logged in user belongs to. - // The type of customer (user, account, custom) - // is chosen during Split's sign-up process. key: 'key' } }); @@ -877,7 +867,7 @@ type SplitView = { ## Listener -The Split Suite sends impression data back to Split servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression listener*. For that purpose, the Suite's configurations have a parameter called `impressionListener` where an implementation of `ImpressionListener` could be added. This implementation **must** define the `logImpression` method and it receives data in the following schema. +The Split Suite sends impression data back to Harness servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression listener*. For that purpose, the Suite's configurations have a parameter called `impressionListener` where an implementation of `ImpressionListener` could be added. This implementation **must** define the `logImpression` method and it receives data in the following schema. | **Name** | **Type** | **Description** | | --- | --- | --- | @@ -1003,7 +993,7 @@ This section describes advanced use cases and features provided by the Suite. ### Instantiate multiple clients -Split supports the ability to release based on multiple traffic types. For example, with traffic types, you can release to `users` in one feature flag and `accounts` in another. If you are unfamiliar with using multiple traffic types, refer to the [Traffic type guide](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) for more information. +FME supports the ability to release based on multiple traffic types. For example, with traffic types, you can release to `users` in one feature flag and `accounts` in another. If you are unfamiliar with using multiple traffic types, refer to the [Traffic type guide](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) for more information. Each Suite client is tied to one specific customer ID at a time, so if you need to roll out feature flags by different traffic types, instantiate multiple clients, one for each traffic type. For example, you may want to roll out the feature `user-poll` by `users` and the feature `account-permissioning` by `accounts`. @@ -1084,7 +1074,7 @@ account_client.track('user', 'ACCOUNT_CREATED'); -The events captured by the RUM Agent are sent to Split servers using the traffic types and keys of the created client. If no traffic type is provided, the traffic type is `user` by default. +The events captured by the RUM Agent are sent to Harness servers using the traffic types and keys of the created client. If no traffic type is provided, the traffic type is `user` by default. :::info[Number of Suite instances] While the Suite does not put any limitations on the number of instances that can be created, we strongly recommend keeping the number of instances down to **one** or **two**. @@ -1095,8 +1085,8 @@ While the Suite does not put any limitations on the number of instances that can You can listen for four different events from the Suite. * `SDK_READY_FROM_CACHE`. This event fires once the Suite is ready to evaluate treatments using a version of your rollout plan cached in localStorage from a previous session (which might be stale). If there is data in localStorage, this event fires almost immediately, since access to localStorage is fast; otherwise, it doesn't fire. -* `SDK_READY`. This event fires once the Suite is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Split servers. -* `SDK_READY_TIMED_OUT`. This event fires if there is no cached version of your rollout plan cached in localStorage, and the Suite could not download the data from Split servers within the time specified by the `readyTimeout` configuration parameter. This event does not indicate that the Suite initialization was interrupted. The Suite continues downloading the rollout plan and fires the `SDK_READY` event when finished. This delayed `SDK_READY` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. +* `SDK_READY`. This event fires once the Suite is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Harness servers. +* `SDK_READY_TIMED_OUT`. This event fires if there is no cached version of your rollout plan cached in localStorage, and the Suite could not download the data from Harness servers within the time specified by the `readyTimeout` configuration parameter. This event does not indicate that the Suite initialization was interrupted. The Suite continues downloading the rollout plan and fires the `SDK_READY` event when finished. This delayed `SDK_READY` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. * `SDK_UPDATE`. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in the Split user interface. The syntax to listen for each event is shown below: @@ -1185,8 +1175,8 @@ The Suite allows you to disable the tracking of events and impressions until use The `userConsent` configuration parameter lets you set the initial consent status of the Suite instance, and the Suite method `UserConsent.setStatus(boolean)` lets you grant (enable) or decline (disable) the dynamic data tracking. There are three possible initial states: - * `'GRANTED'`: the user grants consent for tracking events and impressions. The Suite sends them to Split cloud. This is the default value if `userConsent` param is not defined. - * `'DECLINED'`: the user declines consent for tracking events and impressions. The Suite does not send them to Split cloud. + * `'GRANTED'`: the user grants consent for tracking events and impressions. The Suite sends them to Harness FME servers. This is the default value if `userConsent` param is not defined. + * `'DECLINED'`: the user declines consent for tracking events and impressions. The Suite does not send them to Harness FME servers. * `'UNKNOWN'`: the user neither grants nor declines consent for tracking events and impressions. The Suite tracks them in its internal storage, and eventually either sends them or not if the consent status is updated to `'GRANTED'` or `'DECLINED'` respectively. The status can be updated at any time with the `UserConsent.setStatus` Suite method. @@ -1199,7 +1189,7 @@ var suite = SplitSuite({ }, // Overwrites the initial consent status of the suite instance, which is 'GRANTED' by default. // 'UNKNOWN' status represents that the user has neither granted nor declined consent for tracking data, - // so the suite will locally track data but not send it to Split cloud until consent is changed to 'GRANTED'. + // so the suite will locally track data but not send it to Harness FME servers until consent is changed to 'GRANTED'. userConsent: 'UNKNOWN' }); @@ -1209,10 +1199,10 @@ suite.UserConsent.getStatus() === suite.UserConsent.Status.UNKNOWN; // `setStatus` method lets you update the suite consent status at any moment. // Pass `true` for 'GRANTED' and `false` for 'DECLINED'. -suite.UserConsent.setStatus(true); // Consent status changed from 'UNKNOWN' to 'GRANTED'. Data will be sent to Split cloud. +suite.UserConsent.setStatus(true); // Consent status changed from 'UNKNOWN' to 'GRANTED'. Data will be sent to Harness FME servers. suite.UserConsent.getStatus() === suite.UserConsent.Status.GRANTED; -suite.UserConsent.setStatus(false); // Consent status changed from 'GRANTED' to 'DECLINED'. Data will not be sent to Split cloud. +suite.UserConsent.setStatus(false); // Consent status changed from 'GRANTED' to 'DECLINED'. Data will not be sent to Harness FME servers. suite.UserConsent.getStatus() === suite.UserConsent.Status.DECLINED; ``` @@ -1231,7 +1221,7 @@ import { SplitSuite } from '@splitsoftware/browser-suite'; const suite = SplitSuite({ ... rumAgent: { - // Optional prefix to append to the `eventTypeId` of the events sent to Split. + // Optional prefix to append to the `eventTypeId` of the events sent to Harness. // For example, if you set the prefix to 'my-app', the event type 'error' will be sent as 'my-app.error'. prefix: 'my-app', // The agent posts the queued events data in bulks. This parameter controls the posting rate in seconds. diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/ios-suite.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/ios-suite.md index aa4f0a6b9a9..2d598d79d0a 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/ios-suite.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/client-side-suites/ios-suite.md @@ -9,9 +9,9 @@ helpdocs_is_published: true

-This guide provides detailed information about our iOS Suite, an SDK designed to harness the full power of Split. The iOS Suite is built on top of the [iOS SDK](https://help.split.io/hc/en-us/articles/360020401491-iOS-SDK) and the [iOS RUM Agent](https://help.split.io/hc/en-us/articles/22545155055373-iOS-RUM-Agent), offering a unified solution, optimized for iOS development. +This guide provides detailed information about our iOS Suite, an SDK designed to leverage the full power of FME. The iOS Suite is built on top of the [iOS SDK](https://help.split.io/hc/en-us/articles/360020401491-iOS-SDK) and the [iOS RUM Agent](https://help.split.io/hc/en-us/articles/22545155055373-iOS-RUM-Agent), offering a unified solution, optimized for iOS development. -The Suite provides the all-encompassing essential programming interface for working with your Split feature flags, as well as capabilities for automatically tracking performance measurements and user events. Code currently using iOS SDK or iOS RUM Agent can be easily upgraded to iOS Suite, which is designed as a drop-in replacement. +The Suite provides the all-encompassing essential programming interface for working with your FME feature flags, as well as capabilities for automatically tracking performance measurements and user events. Code currently using iOS SDK or iOS RUM Agent can be easily upgraded to iOS Suite, which is designed as a drop-in replacement. ## Language support @@ -19,11 +19,11 @@ This library is designed for iOS applications written in Swift and is compatible ## Initialization -Set up Split in your code base with the following two steps: +Set up FME in your code base with the following two steps: ### 1. Import the Suite into your project -Add the Split SDK, Split RUM agent, and Split Suite into your project using Swift Package Manager by adding the following package dependencies: +Add the SDK, Split RUM agent, and Split Suite into your project using Swift Package Manager by adding the following package dependencies: - [iOS SDK] (https://github.com/splitio/ios-client), latest version `3.0.0` - [iOS RUM](https://github.com/splitio/ios-rum), latest version `0.4.0` @@ -48,7 +48,7 @@ Then import the Suite in your code. import iOSSplitSuite ``` -### 2. Instantiate the Suite and create a new Split client +### 2. Instantiate the Suite and create a new SDK client In your code, instantiate the Suite client as shown below. @@ -56,7 +56,7 @@ In your code, instantiate the Suite client as shown below. // Create default Suite configuration let config = SplitSuiteConfig() -// Split SDK key +// SDK key let sdkKey = "YOUR_SDK_KEY" let matchingKey = Key(matchingKey: "key") @@ -71,10 +71,10 @@ let client = suite?.client; ``` :::info[Important] -If you are upgrading from Split's iOS RUM Agent to iOS Suite and you have setup or config information for the iOS RUM Agent in the `SplitRumAgent-Info.plist`, then this information will be overridden by the Suite initialization. That is why we recommended that you remove this information from that file when upgrading. +If you are upgrading from FME's iOS RUM Agent to iOS Suite and you have setup or config information for the iOS RUM Agent in the `SplitRumAgent-Info.plist`, then this information will be overridden by the Suite initialization. That is why we recommended that you remove this information from that file when upgrading. ::: -When the Suite is instantiated, it starts synchronizing feature flag and segment definitions from Split servers, and also starts collecting performance and user events for the configured key and its optional traffic type (which if not set, defaults to `'user'`). +When the Suite is instantiated, it starts synchronizing feature flag and segment definitions from Harness servers, and also starts collecting performance and user events for the configured key and its optional traffic type (which if not set, defaults to `'user'`). We recommend instantiating the Suite once as a singleton and reusing it throughout your application. @@ -84,7 +84,7 @@ Configure the Suite with the SDK key for the Split environment that you would li ### Basic use -When the Suite is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds depending on the size of the data. If the Suite is asked to evaluate which treatment to show to a user for a specific feature flag while in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the Suite does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). +When the Suite is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Harness servers. This process can take up to a few hundred milliseconds depending on the size of the data. If the Suite is asked to evaluate which treatment to show to a user for a specific feature flag while in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the Suite does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). To make sure the Suite is properly loaded before asking it for a treatment, block until the Suite is ready, as shown below. We set the client to listen for the `SDK_READY` event triggered by the Suite before asking for an evaluation. @@ -189,7 +189,7 @@ let result = client.clearAttributes() ### Multiple evaluations at once -In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` method of the Split client to do this. +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` method of the SDK client to do this. * `getTreatments`: Pass a list of the feature flag names you want treatments for. * `getTreatmentsByFlagSet`: Evaluate all flags that are part of the provided set name and are cached on the Suite instance. * `getTreatmentsByFlagSets`: Evaluate all flags that are part of the provided set names and are cached on the Suite instance. @@ -265,7 +265,7 @@ The `client.track()` method sends events **_for the identity configured on the c * Contains only letters, numbers, hyphen, underscore, or period. * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}`. * **VALUE:** (Optional) The value used in creating the metric. This field can be sent in as nil or 0 if you intend to only use the count function when creating a metric. The expected data type is `Double`. -* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. +* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. FME currently supports three types of properties: strings, numbers, and booleans. The `suite.track()` method sends events **_for all the identities_** configured on all instances of the Suite clients. For those clients that have not been configured with a traffic type, this `track` method uses the default traffic type `user`. This `track` method can take up to three of the four arguments described above: `EVENT_TYPE`, `VALUE`, and `PROPERTIES`. @@ -308,7 +308,7 @@ let resp = suite.track(eventType: "page_load_time", proerties: properties) ``` -The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event sent back to Split's servers on the next event post. The SDK returns `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method is provided. +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event sent back to Harness servers on the next event post. The SDK returns `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method is provided. ### Shutdown @@ -341,16 +341,16 @@ Feature flagging parameters: | **Configuration** | **Description** | **Default value** | | --- | --- | --- | -| featuresRefreshRate | The Suite polls Split servers for changes to feature flags at this rate (in seconds). | 3600 seconds (1 hour) | -| segmentsRefreshRate | The Suite polls Split servers for changes to segments at this rate (in seconds). | 1800 seconds (30 minutes) | -| impressionRefreshRate | The treatment log captures which customer saw what treatment (on, off, etc.) at what time. This log is periodically flushed back to Split servers. This configuration controls how quickly the cache expires after a write (in seconds). | 1800 seconds (30 minutes) | +| featuresRefreshRate | The Suite polls Harness servers for changes to feature flags at this rate (in seconds). | 3600 seconds (1 hour) | +| segmentsRefreshRate | The Suite polls Harness servers for changes to segments at this rate (in seconds). | 1800 seconds (30 minutes) | +| impressionRefreshRate | The treatment log captures which customer saw what treatment (on, off, etc.) at what time. This log is periodically flushed back to Harness servers. This configuration controls how quickly the cache expires after a write (in seconds). | 1800 seconds (30 minutes) | | impressionsQueueSize | Default queue size for impressions. | 30K | -| eventsPushRate | When using `.track`, how often the events queue is flushed to Split servers. | 1800 seconds| +| eventsPushRate | When using `.track`, how often the events queue is flushed to Harness servers. | 1800 seconds| | eventsPerPush | Maximum size of the batch to push events. | 2000 | | eventsFirstPushWindow | Amount of time to wait for the first flush. | 10 seconds | | eventsQueueSize | When using `.track`, the number of **events** to be kept in memory. | 10000 | | trafficType | (optional) The default traffic type for events tracked using the `track` method. If not specified, every `track` call should specify a traffic type. | not set | -| telemetryRefreshRate | The Suite caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers (in seconds). | 3600 seconds (1 hour) | +| telemetryRefreshRate | The Suite caches diagnostic data that it periodically sends to Harness servers. This configuration controls how frequently this data is sent back to Harness servers (in seconds). | 3600 seconds (1 hour) | | logLevel | Enables logging according to the level specified. Options are `NONE`, `VERBOSE`, `DEBUG`, `INFO`, `WARNING`, and `ERROR`. | `NONE` | | synchronizeInBackground | Activates synchronization when application host is in background. | `false` | | streamingEnabled | Boolean flag to enable the streaming service as default synchronization mechanism when in foreground. In the event of an issue with streaming, the Suite falls back to the polling mechanism. If false, the Suite polls for changes as usual without attempting to use streaming. | `true` | @@ -369,7 +369,7 @@ Suite RUM agent parameters: | **Configuration** | **Description** | **Default value** | | --- | --- | --- | -| prefix | Optional prefix to append to the `eventTypeId` of the events sent to Split by the Suite RUM agent. For example, if you set the prefix to 'my-app', the event type 'error' will be sent as 'my-app.error'. | `nil` | +| prefix | Optional prefix to append to the `eventTypeId` of the events sent to Harness by the Suite RUM agent. For example, if you set the prefix to 'my-app', the event type 'error' will be sent as 'my-app.error'. | `nil` | Shared parameters: @@ -381,7 +381,7 @@ To set each of the parameters defined above, use the following syntax: ```swift title="Swift" import Split -// Your Split SDK key +// Your SDK key let sdkKey: String = "YOUR_SDK_KEY" //User Key @@ -407,7 +407,7 @@ let client = factory?.client ## Localhost mode -For testing, a developer can put code behind feature flags on their development machine without the Suite requiring network connectivity. To achieve this, you can start the Suite in **localhost** mode (aka, off-the-grid mode). In this mode, the Suite neither polls or updates Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the feature flags. To use the Suite in localhost mode, replace the SDK Key with `localhost`, as shown in the example below. +For testing, a developer can put code behind feature flags on their development machine without the Suite requiring network connectivity. To achieve this, you can start the Suite in **localhost** mode (aka, off-the-grid mode). In this mode, the Suite neither polls or updates Harness servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the feature flags. To use the Suite in localhost mode, replace the SDK Key with `localhost`, as shown in the example below. The format for defining the definitions is as follows: @@ -436,7 +436,7 @@ In the example above, we have four entries: In this mode, the Split Suite loads the yaml file from a resource bundle file at the assets' project `src/main/assets/splits.yaml`. ```swift title="Swift" -// Split SDK key must be "localhost" +// SDK key must be "localhost" let apiKey: String = "localhost" let key: Key = Key(matchingKey: "key") let config = SplitClientConfig() @@ -453,7 +453,7 @@ FEATURE_FLAG_NAME TREATMENT You can update feature flag definitions programmatically by using the `updateLocalhost` method, as shown below. ```swift title="Swift" -// Split SDK key must be "localhost" +// SDK key must be "localhost" let apiKey: String = "localhost" let key: Key = Key(matchingKey: "key") let config = SplitClientConfig() @@ -486,7 +486,7 @@ By enabling debug mode, the *localhost* file location is logged to the console, ## Manager -Use the Split Manager to get a list of feature flags available to the Split client. +Use the Split Manager to get a list of feature flags available to the SDK client. To instantiate a Manager in your code base, use the same factory that you used for your client. @@ -552,7 +552,7 @@ public class SplitView: NSObject, Codable { ## Listener -Split Suite sends impression data back to Split servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression handler*. +Split Suite sends impression data back to Harness servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression handler*. The Suite sends the generated impressions to the impression handler right away. As a result, be careful while implementing handling logic to avoid blocking the main thread. Generally speaking, you should create a separate thread to handle incoming impressions. Refer to the snippet below. @@ -598,7 +598,7 @@ In regards with the data available here, refer to the `impression` objects inter ## Flush -The flush() method sends the data stored in memory (impressions and events) to Split cloud and clears the successfully posted data. If a connection issue is experienced, the data will be sent on the next attempt. +The flush() method sends the data stored in memory (impressions and events) to Harness FME servers and clears the successfully posted data. If a connection issue is experienced, the data will be sent on the next attempt. ```swift title="Swift" client.flush() @@ -698,8 +698,8 @@ While the Suite does not put any limitations on the number of `SplitFactory` ins You can listen for four different events from the Suite. * `sdkReadyFromCache`: This event fires once the Suite is ready to evaluate treatments using a locally cached version of your rollout plan from a previous session (which might be stale). If there is data in the cache, this event fires almost immediately, since access to the cache is fast; otherwise, it doesn't fire. -* ` sdkReady`: This event fires once the Suite is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Split servers. -* ` sdkReadyTimedOut`: This event fires if there is no cached version of your rollout plan in disk cache, and the Suite could not fully download the data from Split servers within the time specified by the `sdkReadyTimeOut` property of the `SplitClientConfig` object. This event does not indicate that the Suite initialization was interrupted. The Suite continues downloading the rollout plan and fires the `sdkReady` event when finished. This delayed `sdkReady` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. +* ` sdkReady`: This event fires once the Suite is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Harness servers. +* ` sdkReadyTimedOut`: This event fires if there is no cached version of your rollout plan in disk cache, and the Suite could not fully download the data from Harness servers within the time specified by the `sdkReadyTimeOut` property of the `SplitClientConfig` object. This event does not indicate that the Suite initialization was interrupted. The Suite continues downloading the rollout plan and fires the `sdkReady` event when finished. This delayed `sdkReady` event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations. * `sdkUpdated`: This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in the Split user interface. Split Suite event handling is done through the `on(event:execute:)` function, which receives a closure as an event handler. The code within the closure is executed on the main thread. For that reason, running code in the background must be done explicitly. @@ -768,13 +768,13 @@ let suite = DefaultSplitSuite ### User consent -By default the Suite will send events to Split cloud, but you can disable this behavior until user consent is explicitly granted. +By default the Suite will send events to Harness FME servers, but you can disable this behavior until user consent is explicitly granted. The `userConsent` configuration parameter lets you set the initial consent status of the Suite, and the `suite.setUserConsent(boolean)` method lets you grant (enable) or decline (disable) dynamic event tracking. There are three possible initial states: - * `'GRANTED'`: The user grants consent for tracking events and impressions. The SDK sends them to Split cloud. This is the default value if `userConsent` param is not defined. - * `'DECLINED'`: The user declines consent for tracking events and impressions. The SDK does not send them to Split cloud. + * `'GRANTED'`: The user grants consent for tracking events and impressions. The SDK sends them to Harness FME servers. This is the default value if `userConsent` param is not defined. + * `'DECLINED'`: The user declines consent for tracking events and impressions. The SDK does not send them to Harness FME servers. * `'UNKNOWN'`: The user neither grants nor declines consent for tracking events and impressions. The SDK tracks them in its internal storage, and eventually either sends them or not if the consent status is updated to `'GRANTED'` or `'DECLINED'` respectively. The status can be updated at any time with the `setUserConsent` factory method. @@ -784,11 +784,11 @@ Working with user consent is demonstrated below. ```swift title="User consent: Initial config, getter and setter" // Overwrites the initial consent status of the Suite instance, which is 'GRANTED' by default. // 'UNKNOWN' status represents that the user has neither granted nor declined consent for tracking data, - // so the Suite locally tracks data but not send it to Split cloud until consent is changed to 'GRANTED'. + // so the Suite locally tracks data but not send it to Harness FME servers until consent is changed to 'GRANTED'. let sdkConfig = SplitClientConfig() sdkConfig.userConsent = .unknown - // Split SDK key + // SDK key let sdkKey = "YOUR_SDK_KEY" let matchingKey = Key(matchingKey: "key") @@ -798,9 +798,9 @@ Working with user consent is demonstrated below. .key(matchingKey) .config(sdkConfig).build() - // Changed User Consent status to 'GRANTED'. Data will be sent to Split cloud. + // Changed User Consent status to 'GRANTED'. Data will be sent to Harness FME servers. suite.setUserConsent(enabled: true); - // Changed User Consent status to 'DECLINED'. Data will not be sent to Split cloud. + // Changed User Consent status to 'DECLINED'. Data will not be sent to Harness FME servers. suite.setUserConsent(enabled: false); // The 'getUserConsent' method returns User Consent status. @@ -844,7 +844,7 @@ certBuilder.certificatePinningConfig { host in print("Failed validation for host \(host)") } -// Set the CertificatePinningConfig property for the Split client configuration +// Set the CertificatePinningConfig property for the SDK client configuration let config = SplitClientConfig() config.certificatePinningConfig = certBuilder.build() // you can add other configuration properties here diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-ios-javascript-sdk-client-on-never-runs.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-ios-javascript-sdk-client-on-never-runs.md index d34289349b9..dab55fc76d8 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-ios-javascript-sdk-client-on-never-runs.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-ios-javascript-sdk-client-on-never-runs.md @@ -22,11 +22,11 @@ client.on(SplitEvent.SDK_READY, new SplitEventTask() { ## Root Cause -The SDK_READY event will fire only once when the SDK factory downloads all the information it needs to calculate the treatment from Split cloud If the code above is executed after the SDK_READY event fires, then the block inside will never be executed since SDK_READY event already fired and will not fire again. +The SDK_READY event will fire only once when the SDK factory downloads all the information it needs to calculate the treatment from Harness FME servers If the code above is executed after the SDK_READY event fires, then the block inside will never be executed since SDK_READY event already fired and will not fire again. ## Solution -Even if the cache existed prior to initializing the SDK Factory object, it will always make an http call to Split cloud to sync for any changes before firing SDK_READY event. This means if we execute client.on line immediately after the factory initialization line it will be guaranteed the SDK_READY event fires after client.on is executed. +Even if the cache existed prior to initializing the SDK Factory object, it will always make an http call to Harness FME servers to sync for any changes before firing SDK_READY event. This means if we execute client.on line immediately after the factory initialization line it will be guaranteed the SDK_READY event fires after client.on is executed. We recommend to create a wrapper class for the SDK, define isSDKReady property and set it to true inside the client.on block. Below are examples per each SDK language: diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-calling-client-destroy-does-not-post-impressions.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-calling-client-destroy-does-not-post-impressions.md index b8a6fc1f915..902716cb5de 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-calling-client-destroy-does-not-post-impressions.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-calling-client-destroy-does-not-post-impressions.md @@ -16,11 +16,11 @@ When using Android SDK in an app, before the app exits, calling client.Destroy() ## Root Cause -The `client.Destory()` will post any cached impressions, however, if the app shutdown its process before or during the post request, the request will fail and no impressions are posted to Split cloud. +The `client.Destory()` will post any cached impressions, however, if the app shutdown its process before or during the post request, the request will fail and no impressions are posted to Harness servers cloud. ## Answer To resolve the issue, there are two options: -* This is the recommended action, add to the app workflow calling client.Flush() method which will post the cached impressions and events to Split cloud. +* This is the recommended action, add to the app workflow calling client.Flush() method which will post the cached impressions and events to Harness FME servers. * Add a 2-3 seconds sleep or delay after the `client.Destory()` to allow enough time to post the impressions before the app exits, the amount of seconds will be depend on how fast the network though, it is recommended to adjust it accordingly. diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-does-the-sdk-use-sharedpreferences.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-does-the-sdk-use-sharedpreferences.md index 445f22b6c03..dd6090d2b11 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-does-the-sdk-use-sharedpreferences.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-does-the-sdk-use-sharedpreferences.md @@ -1,6 +1,6 @@ --- -title: "Android SDK: Does the SDK use SharedPreferences on the device to store the Split cache?" -sidebar_label: "Android SDK: Does the SDK use SharedPreferences on the device to store the Split cache?" +title: "Android SDK: Does the SDK use SharedPreferences on the device to store the FME cache?" +sidebar_label: "Android SDK: Does the SDK use SharedPreferences on the device to store the FME cache?" helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 23 @@ -11,7 +11,7 @@ sidebar_position: 23

## Question -Does the Android SDK utilize the SharedPreferences on the device to store the Split cache? +Does the Android SDK utilize the SharedPreferences on the device to store the FME cache? ## Answer -The Android SDK does not use the device SharedPreferences. It stores the Split cache directly on internal storage, in the application's context folder. \ No newline at end of file +The Android SDK does not use the device SharedPreferences. It stores the FME cache directly on internal storage, in the application's context folder. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-duplicate-class-finalizablereferencequeue.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-duplicate-class-finalizablereferencequeue.md index ed96d54747f..89bc244d975 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-duplicate-class-finalizablereferencequeue.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-duplicate-class-finalizablereferencequeue.md @@ -17,7 +17,7 @@ When compiling the App with Android SDK the error below is reported Duplicate class com.google.common.base.FinalizableReferenceQueue$DirectLoader found in modules checkstyle-5.3-all.jar (checkstyle-5.3-all.jar) and guava-18.0.jar (com.google.guava:guava:18.0) ## Root Cause -Split Android SDK has Google guava 18.0 library as a dependency, while Checkstyle 5.3 has dependency on [com.google.collections](https://mvnrepository.com/artifact/com.google.collections) » [google-collections](https://mvnrepository.com/artifact/com.google.collections/google-collections) 1.0 which is an old library and is causing the duplicate error. +Android SDK has Google guava 18.0 library as a dependency, while Checkstyle 5.3 has dependency on [com.google.collections](https://mvnrepository.com/artifact/com.google.collections) » [google-collections](https://mvnrepository.com/artifact/com.google.collections/google-collections) 1.0 which is an old library and is causing the duplicate error. ## Solution diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-http-exception-chain-validation-failed.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-http-exception-chain-validation-failed.md index 9800572ae87..eac0c8a26a7 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-http-exception-chain-validation-failed.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-http-exception-chain-validation-failed.md @@ -12,7 +12,7 @@ sidebar_position: 18 ## Issue -When running Android app in Emulator, Split Android SDK shows the error below right after initialization: +When running Android app in Emulator, Android SDK shows the error below right after initialization: ``` io.split.android.client.network.HttpException: HttpException: Error serializing request body: Chain validation failed ``` diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-sdk-takes-too-long-to-get-ready.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-sdk-takes-too-long-to-get-ready.md index bc17d0f583e..30c3c2b4557 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-sdk-takes-too-long-to-get-ready.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-sdk-takes-too-long-to-get-ready.md @@ -12,17 +12,17 @@ sidebar_position: 16 ## Issue -When using Android SDK, the first time the App loads the SDK takes sometime to download definitions from Split cloud and cache them locally. However, when SDK starts afterwards, it still takes long time even though the cache is already downloaded to app file system. +When using Android SDK, the first time the App loads the SDK takes sometime to download definitions from Harness FME servers and cache them locally. However, when SDK starts afterwards, it still takes long time even though the cache is already downloaded to app file system. ## Root Cause -If the version of Android SDK used is 2.4.2 or below, the issue can manifest since the factory object is still making a full data request from Split cloud even if the previous cache exists in app file system. +If the version of Android SDK used is 2.4.2 or below, the issue can manifest since the factory object is still making a full data request from Harness FME servers even if the previous cache exists in app file system. ## Solution Upgrade Android SDK to latest build to fix this issue, -To prevent your app from waiting indefinitely on Split SDK in case there is an issue with the network, you can listen to SDK_READY_TIMED_OUT with a specific timeout you can set. This will allow your code to move on and not continue to wait on Split SDK. +To prevent your app from waiting indefinitely on the SDK in case there is an issue with the network, you can listen to SDK_READY_TIMED_OUT with a specific timeout you can set. This will allow your code to move on and not continue to wait on the SDK. Another useful event is SDK_READY_FROM_CACHE, since the first time the SDK runs successfully in the app it will store the cache in the app storage, so the next time the SDK initializes it can use the existing cache and does not need to wait for the network sync. diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-using-kotlin-sdk-always-returns-control-treatment.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-using-kotlin-sdk-always-returns-control-treatment.md index 8f2293d28bc..f3e9b0c87cc 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-using-kotlin-sdk-always-returns-control-treatment.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/android-sdk-using-kotlin-sdk-always-returns-control-treatment.md @@ -12,7 +12,7 @@ sidebar_position: 17 ## Issue -When using Android App with Kotlin language, the code below always returns contro" treatment from Split Android SDK +When using Android App with Kotlin language, the code below always returns contro" treatment from Android SDK ```java val apiKey = "API KEY" diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-and-android-sdk-how-to-initialize-for-multiple-user-ids.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-and-android-sdk-how-to-initialize-for-multiple-user-ids.md index 9faf4abf282..8d0e5b4519a 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-and-android-sdk-how-to-initialize-for-multiple-user-ids.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-and-android-sdk-how-to-initialize-for-multiple-user-ids.md @@ -12,7 +12,7 @@ sidebar_position: 19 ## Question -The JavaScript SDK is capable of initializing multiple client objects from the same Split factory object, each with their unique user key (user id): +The JavaScript SDK is capable of initializing multiple client objects from the same SDK factory object, each with their unique user key (user id): ```javascript client1 = factory.client("user_id1"); diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-android-browser-sdk-does-the-sdk-cache-expire.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-android-browser-sdk-does-the-sdk-cache-expire.md index 5818f4115e2..3a0a3e6a5dd 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-android-browser-sdk-does-the-sdk-cache-expire.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-android-browser-sdk-does-the-sdk-cache-expire.md @@ -12,8 +12,8 @@ sidebar_position: 8 ## Question -The Split mobile (iOS and Android) and JavaScript browser SDKs download a local cache and store it in a file system. Does the cache have an expire date or TTL? +The Split mobile (iOS and Android) and JavaScript Browser SDKs download a local cache and store it in a file system. Does the cache have an expire date or TTL? ## Answer -The SDK will consider the cache stale if it hasn't been updated for 90 days. In such case it will issue a full download of Split definitions. This is an unlikely scenario since the SDK is continuously synching changes from the Split cloud and updating the cache. \ No newline at end of file +The SDK will consider the cache stale if it hasn't been updated for 90 days. In such case it will issue a full download of FME definitions. This is an unlikely scenario since the SDK is continuously synching changes from the Harness FME servers and updating the cache. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-android-browser-sdk-split-changes-roll-out-slowly-to-user-devices.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-android-browser-sdk-fme-changes-roll-out-slowly-to-user-devices.md similarity index 83% rename from docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-android-browser-sdk-split-changes-roll-out-slowly-to-user-devices.md rename to docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-android-browser-sdk-fme-changes-roll-out-slowly-to-user-devices.md index f66e2fead47..e4108fe0849 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-android-browser-sdk-split-changes-roll-out-slowly-to-user-devices.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-android-browser-sdk-fme-changes-roll-out-slowly-to-user-devices.md @@ -1,6 +1,6 @@ --- -title: "Mobile and web SDK: Split changes roll out slowly to user devices" -sidebar_label: "Mobile and web SDK: Split changes roll out slowly to user devices" +title: "Mobile and web SDK: FME changes roll out slowly to user devices" +sidebar_label: "Mobile and web SDK: FME changes roll out slowly to user devices" helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 7 @@ -14,7 +14,7 @@ sidebar_position: 7 When making a change to a feature flag through the web UI, mobile (iOS and Android) and JavaScript Browser SDKs do not reflect that change at the same time. A small population of devices are synched in the first day, then more user devices get synched in subsequent days until all SDKs are updated. -Why do Split changes propagate slowly to user devices? +Why do FME changes propagate slowly to user devices? ## Root Cause @@ -25,6 +25,6 @@ This scenario has two potential root causes: ## Solution -Always calculate treatments after `SDK_READY` event fires. This event fires once the synchronization with Split cloud is complete and will guarantee the latest changes are reflected in the cache. +Always calculate treatments after `SDK_READY` event fires. This event fires once the synchronization with Harness FME servers is complete and will guarantee the latest changes are reflected in the cache. While `SDK_READY_FROM_CACHE` is very useful to allow calculating treatments quickly, it is recommended to check the treatment again after `SDK_READY` and reflect the treatment in case there is a change. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-sdk-missing-track-method.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-sdk-missing-track-method.md index fa997310983..d1329e96348 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-sdk-missing-track-method.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-sdk-missing-track-method.md @@ -12,12 +12,12 @@ sidebar_position: 27 ## Issue -Using Split iOS SDK in Xcode project, when trying to use track method, build error "Value of type 'SplitClientProtocol' has no member 'track'. +Using iOS SDK in Xcode project, when trying to use track method, build error "Value of type 'SplitClientProtocol' has no member 'track'. ![](https://help.split.io/hc/article_attachments/360010664231/Screen_Shot_2018-09-04_at_9.36.57_AM.png) ## Root Cause -The Split iOS SDK used is likely an old version that is earlier than 1.3.0. +The iOS SDK used is likely an old version that is earlier than 1.3.0. ## Solution Make sure to use the latest version in Cocoapod. Refer to the [SDK doc link](https://docs.split.io/docs/ios-sdk-overview). \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-sdk-runtime-error-jfbcrypt-m-left-shift-of-x-by-y-places.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-sdk-runtime-error-jfbcrypt-m-left-shift-of-x-by-y-places.md index 2a4bc6ccb5c..7acce0781cf 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-sdk-runtime-error-jfbcrypt-m-left-shift-of-x-by-y-places.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/ios-sdk-runtime-error-jfbcrypt-m-left-shift-of-x-by-y-places.md @@ -12,7 +12,7 @@ sidebar_position: 13 ## Issue -Using Objective-C project with iOS SDK, the following runtime error shows as soon as the Split factory object is initialized: +Using Objective-C project with iOS SDK, the following runtime error shows as soon as the SDK factory object is initialized: ``` .../Pods/Split/Split/Common/Utils/JFBCrypt/JFBCrypt.m:578:16: runtime error: left shift of 16488694 by 8 places cannot be represented in type 'SInt32' (aka 'int') diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-how-to-enable-conent-security-policy.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-how-to-enable-conent-security-policy.md index ba3541625ac..99c6e689c8f 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-how-to-enable-conent-security-policy.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-how-to-enable-conent-security-policy.md @@ -12,13 +12,13 @@ sidebar_position: 21 ## Question -Is it possible to enable SCP (Content Security Policy) on a site that uses Split JavaScript SDK? +Is it possible to enable SCP (Content Security Policy) on a site that uses JavaScript SDK? ## Answer Content Security Policy (CSP) is a computer security standard introduced to prevent cross-site scripting (XSS), clickjacking and other code injection attacks, as defined by this wikipedia article. -It is possible to allow SCP and enable running Split JavaScript SDK safely. +It is possible to allow SCP and enable running JavaScript SDK safely. There are multiple ways to achieve this, the steps below use "nonce" keyword to target the script block. diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-localhost-mode-does-not-support-allowlist-keys.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-localhost-mode-does-not-support-allowlist-keys.md index 2820052c92f..4c288c49f9f 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-localhost-mode-does-not-support-allowlist-keys.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-localhost-mode-does-not-support-allowlist-keys.md @@ -14,7 +14,7 @@ sidebar_position: 2 JavaScript, React, Redux, and Browser SDKs use features config parameter to set the feature flags and treatments names, however, it does not support adding Allowlist keys in the property. -How can we mimic allowing keys to get certain treatments similar to the yaml file structure used for Server side SDKs? +How can we mimic allowing keys to get certain treatments similar to the yaml file structure used for server-side SDKs? ## Answer diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-not-ready-status-in-slow-networks.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-not-ready-status-in-slow-networks.md index 8a4de875927..0086baef824 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-not-ready-status-in-slow-networks.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/javascript-sdk-not-ready-status-in-slow-networks.md @@ -16,17 +16,17 @@ When using JavaScript SDK in Browser, the SDK status will mostly return Not Read ## Root Cause -It takes a long time for the SDK to fetch the feature flags and Segments Information data from Split cloud due to slow network, which might cause control treatments. +It takes a long time for the SDK to fetch the feature flags and Segments Information data from Harness FME servers due to slow network, which might cause control treatments. ## Solution -You need to Increase the startup.readyTimeout value to ensure it covers the SDK fetching Split configuration time. +You need to Increase the startup.readyTimeout value to ensure it covers the FME definition fetching time. As explained in https://docs.split.io/docs/javascript-sdk-overview under Configuration section, the default value for startup.requestTimeoutBeforeReady is 1.5 seconds. Follow the steps below to implement the solution: -1. Find out how long it takes the browser to fetch the Split configuration under slow Network, Chrome Dev tools can be used to simulate 3G Network. +1. Find out how long it takes the browser to fetch the FME definition under slow Network, Chrome Dev tools can be used to simulate 3G Network. 2. Make sure to enable the Java SDK console debug logging by running the following command in the browser JavaScript console: ``` localStorage.splitio_debug = 'on' @@ -45,7 +45,7 @@ startup: { readyTimeout: 5 }, ``` -5. In addition to the above, enable using the browser cache to store the Split configuration, to avoid using the network each time the Split configuration data is needed. You only need to specify the the structure below when initializing your SDK object: +5. In addition to the above, enable using the browser cache to store the FME definition, to avoid using the network each time the FME definition data is needed. You only need to specify the the structure below when initializing your SDK object: ``` storage: { type: 'LOCALSTORAGE', diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-error-building-app-with-webpack.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-error-building-app-with-webpack.md index dcefc99917d..44cc3d00f77 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-error-building-app-with-webpack.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-error-building-app-with-webpack.md @@ -35,7 +35,8 @@ i ?wdm?: Failed to compile. Webpack is trying to build the React SDK library into the server side, which will cause errors since React SDK is designed to run only on browser side. -Answer +## Answer + To resolve the issue, open `webpack.config.js` file, locate the resolve section, make sure the `mainFields` entry contain `browser` similar to this example: ```json resolve: { diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-is-it-possible-to-get-treatments-outside-the-components.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-is-it-possible-to-get-treatments-outside-the-components.md index a3b1dde19aa..ca097ba0298 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-is-it-possible-to-get-treatments-outside-the-components.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-is-it-possible-to-get-treatments-outside-the-components.md @@ -12,7 +12,7 @@ sidebar_position: 5 ## Question -Using the React SDK, is it possible to get Split treatments through JavaScript code in addition to using the SDK React components? +Using the React SDK, is it possible to get feature flag treatments through JavaScript code in addition to using the SDK React components? ## Answer diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-istimeout-prop-not-returning-true-when-react-sdk-times-out.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-istimeout-prop-not-returning-true-when-react-sdk-times-out.md index ba1f35c119b..aeefe3260c3 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-istimeout-prop-not-returning-true-when-react-sdk-times-out.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-istimeout-prop-not-returning-true-when-react-sdk-times-out.md @@ -12,7 +12,7 @@ sidebar_position: 3 ## Issue -When using Split React SDK, it is recommended to check if the SDK has timed out within a specific timeout before it finish downloading the cache and signal its ready. +When using React SDK, it is recommended to check if the SDK has timed out within a specific timeout before it finish downloading the cache and signal its ready. For the example below, the code does not display the message when SDK has timed-out: ```javascript diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-lazy-initialization-of-split-client.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-lazy-initialization-of-split-client.md index bf92c836cdf..96756b304ee 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-lazy-initialization-of-split-client.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/react-sdk-lazy-initialization-of-split-client.md @@ -1,6 +1,6 @@ --- -title: "React SDK: Lazy initialization of Split client" -sidebar_label: "React SDK: Lazy initialization of Split client" +title: "React SDK: Lazy initialization of SDK client" +sidebar_label: "React SDK: Lazy initialization of SDK client" helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 12 @@ -12,7 +12,7 @@ sidebar_position: 12 ## Question -When using React app, on initial load of a client-side application the Split key is not always directly available. The React SDK will initialize SplitFactory and useClient on the initial render, which means that with the current setup we have to initiate the Split client with a key that might not exist yet. +When using React app, on initial load of a client-side application the FME key is not always directly available. The React SDK will initialize SplitFactory and useClient on the initial render, which means that with the current setup we have to initiate the SDK client with a key that might not exist yet. ## Answer diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/redux-sdk-control-treatment-returned-when-sdk-is-initialized.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/redux-sdk-control-treatment-returned-when-sdk-is-initialized.md index 2ba7d95f508..e3f35664072 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/redux-sdk-control-treatment-returned-when-sdk-is-initialized.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-client-side-sdks/redux-sdk-control-treatment-returned-when-sdk-is-initialized.md @@ -30,7 +30,7 @@ export default function initialise() { ## Root Cause -When the SDK initializes, it starts downloading the cache from Split cloud, during this time isReady is false, if we try fetching treatments at that point, we will get control. We also need to evaluate updating isReady flag to true once the SDK is ready asynchronously. +When the SDK initializes, it starts downloading the cache from Harness FME servers, during this time isReady is false, if we try fetching treatments at that point, we will get control. We also need to evaluate updating isReady flag to true once the SDK is ready asynchronously. ## Solution diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/always-getting-control-treatments.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/always-getting-control-treatments.md index fde94b3b4ff..70df73a8a7f 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/always-getting-control-treatments.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/always-getting-control-treatments.md @@ -18,14 +18,14 @@ When using SDK, control treatment is either always or very often returned from ` When `getTreatment` call returns `control`, this means either: -* The there is an issue with network connection to Split cloud and http calls are timing out. Enable the SDK debugging log file to verify if there are any network errors. -* The SDK is still downloading relevant feature flag definitions and Segments from Split cloud and still did not finish when `getTreatment` call is executed. +* The there is an issue with network connection to Harness FME servers and http calls are timing out. Enable the SDK debugging log file to verify if there are any network errors. +* The SDK is still downloading relevant feature flag definitions and Segments from Harness FME servers and still did not finish when `getTreatment` call is executed. ## Solution The `control` treatment is most likely to return using the mobile SDKs; JavaScript, Android and iOS. Simply because potentially the SDK runs on users' mobile devices which may have a slow network connection. -That is why for these SDKs `getTreatment` should always be called when the SDK_READY events fires, which will ensure it's called after the SDK downloads all the information from Split cloud and avoid returning `control` treatments. +That is why for these SDKs `getTreatment` should always be called when the SDK_READY events fires, which will ensure it's called after the SDK downloads all the information from Harness FME servers and avoid returning `control` treatments. ``` client.on(client.Event.SDK_READY, function() { diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/error-gettreatment-you-passed-split-name-that-does-not-exist-in-this-environment.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/error-gettreatment-you-passed-split-name-that-does-not-exist-in-this-environment.md index 2a565391085..b2804aaae22 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/error-gettreatment-you-passed-split-name-that-does-not-exist-in-this-environment.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/error-gettreatment-you-passed-split-name-that-does-not-exist-in-this-environment.md @@ -12,7 +12,7 @@ sidebar_position: 8 ##Problem -When using Split SDK and calling getTreatment for a list of feature flags names, there are lot of errors raised as below +When using an FME SDK and calling getTreatment for a list of feature flags names, there are lot of errors raised as below ``` admin 10 May 2019, 18:10:12 2019-05-10T17:10:12,445 ERROR [admin] [f0f338a964a0e3e1/07cfe07d08568096] [SplitClientImpl:256] - getTreatment: you passed "SPLIT NAME" that does not exist in this environment, please double check what Splits exist in the web console. diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/how-do-i-find-out-what-changed-in-an-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/how-do-i-find-out-what-changed-in-an-sdk.md index 190176b434e..cac55350c11 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/how-do-i-find-out-what-changed-in-an-sdk.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/how-do-i-find-out-what-changed-in-an-sdk.md @@ -12,7 +12,7 @@ sidebar_position: 11 ## Question -How do I find out what changed when Split releases a new version of the SDK? +How do I find out what changed when Harness releases a new version of an FME SDK? ### Answer diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/how-to-ensure-sdk-is-configured-to-handle-the-generated-impressions-and-events-load.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/how-to-ensure-sdk-is-configured-to-handle-the-generated-impressions-and-events-load.md index e1b2544d529..ba78ce9da31 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/how-to-ensure-sdk-is-configured-to-handle-the-generated-impressions-and-events-load.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/how-to-ensure-sdk-is-configured-to-handle-the-generated-impressions-and-events-load.md @@ -10,7 +10,7 @@ sidebar_position: 7

-By default, all Split SDKs have default configuration that allows them to process a heavy load of generated Impressions and Events. These config parameters values are documented in the help section of each SDK. +By default, all FME SDKs have default configuration that allows them to process a heavy load of generated Impressions and Events. These config parameters values are documented in the help section of each SDK. For example, if we take a look at the [Java SDK](https://help.split.io/hc/en-us/articles/360020405151-Java-SDK), the Configuration section has the following parameters and their default values for posting impressions: diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/how-to-use-split-sdks-with-split-proxy.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/how-to-use-split-sdks-with-split-proxy.md index 81a36bd2a35..939d650b9f7 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/how-to-use-split-sdks-with-split-proxy.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/how-to-use-split-sdks-with-split-proxy.md @@ -1,6 +1,6 @@ --- -title: "General SDK: How to use Split SDKs with Split Proxy?" -sidebar_label: "General SDK: How to use Split SDKs with Split Proxy?" +title: "General SDK: How to use FME SDKs with Split Proxy?" +sidebar_label: "General SDK: How to use FME SDKs with Split Proxy?" helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 3 @@ -12,7 +12,7 @@ sidebar_position: 3 ## Question -All Split SDKs support connecting to Split Proxy. +All FME SDKs support connecting to Split Proxy. What are the updates needed for each SDK to accomplish this? diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/is-it-possible-to-use-postman-to-calculate-a-treatment-for-a-feature-flag.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/is-it-possible-to-use-postman-to-calculate-a-treatment-for-a-feature-flag.md index 4371e9f9ade..ee052aa99f5 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/is-it-possible-to-use-postman-to-calculate-a-treatment-for-a-feature-flag.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/is-it-possible-to-use-postman-to-calculate-a-treatment-for-a-feature-flag.md @@ -17,8 +17,8 @@ When using Postman in developing environment, and knowing the SDK HTTP calls, ca ## Answer -No. While Postman can use the same HTTP calls to download the feature flags definitions from the Split cloud, it needs to use the same Murmur hash that all SDKs use to assign a bucket (from 1 to 100) for a given user id, then apply the feature flag rules and conditions based on that bucket. +No. While Postman can use the same HTTP calls to download the feature flags definitions from the Harness FME servers, it needs to use the same Murmur hash that all SDKs use to assign a bucket (from 1 to 100) for a given user id, then apply the feature flag rules and conditions based on that bucket. -This process is done by the SDK locally, and not through the Split cloud, which is why we need the SDK libraries. +This process is done by the SDK locally, and not through the Harness FME servers, which is why we need the SDK libraries. As a workaround, Split evaluator can be installed in the environment and Postman can use HTTP get requests to fetch a treatment for given feature flag and user id. The Split evaluator will perform the calculation and respond back to Postman with the corresponding treatment. Check out this [link](https://help.split.io/hc/en-us/articles/360020037072-Split-evaluator) for more info. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/isomorphic-javascript-wrapper-example.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/isomorphic-javascript-wrapper-example.md index c85c6a3bbec..6d796295c8b 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/isomorphic-javascript-wrapper-example.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/isomorphic-javascript-wrapper-example.md @@ -17,7 +17,7 @@ The concept gained traction in the early 2010s, with the rise of Node.js, which By bridging the gap between server and client, isomorphic JavaScript significantly improved application performance, SEO, and overall user experience, becoming a fundamental approach in modern web development. -In the [isomorphic_js_wrapper_demo code example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/JavaScript-Isomorphic-Wrapper), we show that the Split JavaScript SDK is, indeed, isomorphic. +In the [isomorphic_js_wrapper_demo code example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/JavaScript-Isomorphic-Wrapper), we show that the JavaScript SDK is, indeed, isomorphic. The demo evaluates flags on both the server side and the client side using the same SDK Wrapper. This allows you to maintain only a single codebase for wrapping Split's SDK and ensures that you use the proper methods when on the server and the client with the same code. diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/sdk-never-gets-ready-regardless-of-the-ready-timeout-value.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/sdk-never-gets-ready-regardless-of-the-ready-timeout-value.md index 00f7e50bdde..b5b741420ff 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/sdk-never-gets-ready-regardless-of-the-ready-timeout-value.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/sdk-never-gets-ready-regardless-of-the-ready-timeout-value.md @@ -12,13 +12,13 @@ sidebar_position: 9 ## Issue -Split SDK never gets ready, regardless of how much the ready timeout value. +The SDK never gets ready, regardless of how much the ready timeout value. ## Root Cause There are several possible root causes for this issue: -* If the SDK used is a server side type (Python, Ruby, GO, PHP, Node.js or Java), and the API key used is Client-side type. The Split cloud service is expecting a specific call for Segment information which is different for Client-side vs Server-side API keys. +* If the SDK used is a server side type (Python, Ruby, GO, PHP, Node.js or Java), and the API key used is Client-side type. The Harness FME servers service is expecting a specific call for Segment information which is different for Client-side vs Server-side API keys. * Verify if there are large Segments in Split environment. Segments that contain tens of thousands of records will require a long time to be downloaded to the SDK cache. * Verify network connection to sdk.split.io is fast. Use the command below to verify: ``` diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/sdk-readiness-always-times-out-when-running-in-kubernetes-and-istio-proxy.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/sdk-readiness-always-times-out-when-running-in-kubernetes-and-istio-proxy.md index 75bd052f471..23240e19a83 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/sdk-readiness-always-times-out-when-running-in-kubernetes-and-istio-proxy.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/sdk-readiness-always-times-out-when-running-in-kubernetes-and-istio-proxy.md @@ -12,7 +12,7 @@ sidebar_position: 2 ## Issue -Running an application that uses Split SDK in a Kubernetes container that is configured to use Istio proxy always results in SDK not ready exception. +Running an application that uses FME SDK in a Kubernetes container that is configured to use Istio proxy always results in SDK not ready exception. When enabling the SDK debug log files, it appears the SDK http calls are erroring out with **connection refused** error diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/why-are-impressions-not-showing-in-split.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/why-are-impressions-not-showing-in-split.md index 222f3263691..a2cf1a4e2db 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/why-are-impressions-not-showing-in-split.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/why-are-impressions-not-showing-in-split.md @@ -1,6 +1,6 @@ --- -title: Why are impressions not showing in Split? -sidebar_label: Why are impressions not showing in Split? +title: Why are impressions not showing in Harness FME? +sidebar_label: Why are impressions not showing in Harness FME? helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 12 @@ -12,7 +12,7 @@ sidebar_position: 12 ## Issue -When using any SDK and calling the get treatment method, the call returns a correct treatment value. However, the impression is not getting sent to Split server and does not show up in Results page. +When using any SDK and calling the get treatment method, the call returns a correct treatment value. However, the impression is not getting sent to Harness FME servers and does not show up on the Results page. ## Root Cause @@ -23,9 +23,9 @@ When using Redis and Split Synchronizer: * Synchronizer is not able to read the Impression key in Redis. Check for any errors in the Synchronizer debug log or Synchronizer admin console (http://[Synchronizer host]:3010//admin/dashboard) to determine root cause. * Synchronizer is not keeping up with the impressions flowing from the SDK. Check the [KB article](https://help.split.io/hc/en-us/articles/360016299232-Configure-Split-Synchronizer-for-high-load-Impressions) for solution. -When the SDK connects directly to Split Cloud: +When the SDK connects directly to Harness FME servers: -* All SDKs have a thread that runs frequently and checks the SDK cache for unpublished events and impressions. The frequency is controlled by the impressionsRefreshRate parameter for Impressions, and eventsPushRate for Events. If the SDK code exits while there is still unpublished cache, they will not be posted to Split Cloud. +* All SDKs have a thread that runs frequently and checks the SDK cache for unpublished events and impressions. The frequency is controlled by the impressionsRefreshRate parameter for Impressions, and eventsPushRate for Events. If the SDK code exits while there is still unpublished cache, they will not be posted to Harness servers. * The Key Id (Customer, account, etc) used for the impression has more than 250 characters. * If the SDK is running in an application environment that does not support multi-threading (like Ruby Unicorn and Python gunicorn), then only the main thread will run to calculate the treatments, but the post impressions thread will not run. diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/why-is-the-sdk-making-hundreds-of-network-calls-without-using-gettreatment-or-track-methods.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/why-is-the-sdk-making-hundreds-of-network-calls-without-using-gettreatment-or-track-methods.md index 6f2731000ea..d4fc20f64e8 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/why-is-the-sdk-making-hundreds-of-network-calls-without-using-gettreatment-or-track-methods.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-general-sdk/why-is-the-sdk-making-hundreds-of-network-calls-without-using-gettreatment-or-track-methods.md @@ -12,7 +12,7 @@ sidebar_position: 13 ## Problem -Using any Split SDK library, the Split library is making hundreds of network calls to split.io without using getTreatment or track methods +Using any FME SDK library, the library is making hundreds of network calls to split.io without using getTreatment or track methods ## Root Cause @@ -41,7 +41,7 @@ mySplit3 = new SplitIO(); ## Solution -We always recommend using a singleton factory object, and one client object especially if we are using only one traffic type and customer id. If we need to change either, then its recommended to initiate the client object only, as in the example below: +We always recommend using a singleton factory object, and one client object especially if we are using only one traffic type and customer ID. If we need to change either, then its recommended to initiate the client object only, as in the example below: ```javascript class SplitIO { diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/how-to-inject-a-certificate-into-a-synchronizer-docker-image.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/how-to-inject-a-certificate-into-a-synchronizer-docker-image.md index 55701b97e45..f44823b790d 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/how-to-inject-a-certificate-into-a-synchronizer-docker-image.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/how-to-inject-a-certificate-into-a-synchronizer-docker-image.md @@ -12,7 +12,7 @@ sidebar_position: 3 ## Question -If the Synchronizer Docker container is running in a network that has a proxy using SSL for all traffic, the Synchronizer docker might not be able to authenticate the root certification, which will result in the error below when Synchronizer tries to connect to Split cloud to fetch the feature flags definitions: +If the Synchronizer Docker container is running in a network that has a proxy using SSL for all traffic, the Synchronizer docker might not be able to authenticate the root certification, which will result in the error below when Synchronizer tries to connect to Harness FME servers to fetch the feature flags definitions: ``` SPLITIO-AGENT | ERROR: 2020/08/19 14:42:51 fetchdataforproxy.go:209: Error fetching split changes Get https://sdk.split.io/api/splitChanges?since=-1: x509: certificate signed by unknown authority ``` diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/http-error-when-using-proxy-mode.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/http-error-when-using-proxy-mode.md index 5e67947a540..f8199c93551 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/http-error-when-using-proxy-mode.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/http-error-when-using-proxy-mode.md @@ -14,13 +14,13 @@ sidebar_position: 2 Synchronizer returns 500 HTTP error when used in proxy mode. -Using Synchronizer in proxy mode, when trying to initialize a Split SDK factory connecting to the Synchronizer instance, the SDK never gets ready. +Using Synchronizer in proxy mode, when trying to initialize an FME SDK factory connecting to the Synchronizer instance, the SDK never gets ready. ## Root Cause -Looking at the Synchronizer debug log below, looks like the Synchronizer's call to Split cloud is successful but the JSON structure returns empty feature flags names. +Looking at the Synchronizer debug log below, looks like the Synchronizer's call to Harness FME servers is successful but the JSON structure returns empty feature flags names. -While the HTTP call to Split cloud did not error out, the Synchronizer returns a 500 error to SDK, since getting an empty feature flag list means no flags where added to the environment corresponding to the SDK API key used. This will result in the current SDK session to be unable to calculate any treatments. +While the HTTP call to Harness FME servers did not error out, the Synchronizer returns a 500 error to SDK, since getting an empty feature flag list means no flags where added to the environment corresponding to the SDK API key used. This will result in the current SDK session to be unable to calculate any treatments. ``` SPLITIO-AGENT - DEBUG - 2020/10/12 21:41:51 logger.go:35: GET |500| 285.71µs | 10.10.6.249 | /api/splitChanges diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/no-impressions-sent-from-python-sdk-7.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/no-impressions-sent-from-python-sdk-7.md index aee48eb2646..e98c5f79781 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/no-impressions-sent-from-python-sdk-7.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/no-impressions-sent-from-python-sdk-7.md @@ -11,7 +11,7 @@ sidebar_position: 7

## Issue -When using Synchronizer 1.x version and Python SDK 7.x version, the Python SDK is processing treatments correctly, Synchronizer does not report any errors, but no Impressions are sent to Split cloud. +When using Synchronizer 1.x version and Python SDK 7.x version, the Python SDK is processing treatments correctly, Synchronizer does not report any errors, but no Impressions are sent to Harness FME servers. ## Answer As of Python SDK 7.0.0, design changes where made to match the enhancements made to Synchronizer 2.0 version. Thus, when Python SDK 7.x used, the Synchronizer used must be upgraded to 2.x version. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/post-method-404.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/post-method-404.md index bb5fb5e0dcb..43b9ede9431 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/post-method-404.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/post-method-404.md @@ -19,7 +19,7 @@ After starting Split Synchronizer process (version 1.6.0 and above), Synchronize ## Root Cause -The Error is due to incorrect Split API key passed to the Synchronizer. Which caused the Synchronizer inability to find to the Account in the Split cloud. +The Error is due to incorrect API key passed to the Synchronizer. Which caused the Synchronizer inability to find to the Account in the Harness FME servers. ## Solution @@ -35,7 +35,7 @@ Second, make sure to pass the API key. There are many ways to do it: -api-key ``` -* In a JSON file that Synchronizer uses for configuration. The `apiKey` property is the one that will be used to issue requests against Split Cloud. +* In a JSON file that Synchronizer uses for configuration. The `apiKey` property is the one that will be used to issue requests against Harness FME servers. ![](https://help.split.io/hc/article_attachments/360013671132) @@ -43,7 +43,7 @@ Second, make sure to pass the API key. There are many ways to do it: ![](https://help.split.io/hc/article_attachments/360013671492) -* If the Synchronizer is running within the Split packaged docker image, make sure to use the parameter below: +* If the Synchronizer is running within the packaged docker image, make sure to use the parameter below: ``` -e SPLIT_SYNC_API_KEY ``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/running-evaluator-proxy-synchronizer-k8.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/running-evaluator-proxy-synchronizer-k8.md index a1728972902..8a1ccbd0d0a 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/running-evaluator-proxy-synchronizer-k8.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/running-evaluator-proxy-synchronizer-k8.md @@ -10,6 +10,6 @@ sidebar_position: 1

-Split provides three containerized applications that can be run on your own infrastructure. These apps can handle specific use cases for feature flagging and experimentation with our SDKs and APIs. +Harness FME provides three containerized applications that can be run on your own infrastructure. These apps can handle specific use cases for feature flagging and experimentation with our SDKs and APIs. Learn more by reading the blog post, [Kubernetes and Split](https://www.split.io/blog/kubernetes-and-split/), which provides architectural diagrams and sample configuration files to run [Split Evaluator](https://help.split.io/hc/en-us/articles/360020037072-Split-Evaluator), [Split Proxy](https://help.split.io/hc/en-us/articles/4415960499213-Split-Proxy) and [Split Synchronizer](https://help.split.io/hc/en-us/articles/360019686092-Split-Synchronizer) in Kubernetes. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/using-sdk-sync-gettreatment-control.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/using-sdk-sync-gettreatment-control.md index 5d3b4bebda4..1be08c78e1d 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/using-sdk-sync-gettreatment-control.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-optional-infra/using-sdk-sync-gettreatment-control.md @@ -1,6 +1,6 @@ --- -title: Using SDK with Synchronizer docker, getTreatment is always returning 'control' -sidebar_label: Using SDK with Synchronizer docker, getTreatment is always returning 'control' +title: Using FME SDK with Synchronizer docker, getTreatment is always returning 'control' +sidebar_label: Using FME SDK with Synchronizer docker, getTreatment is always returning 'control' helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 6 diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/go-sdk-error-flushing-storage-queue.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/go-sdk-error-flushing-storage-queue.md index 67ced08c7c5..fe1fe7ce2d6 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/go-sdk-error-flushing-storage-queue.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/go-sdk-error-flushing-storage-queue.md @@ -26,8 +26,8 @@ The SDK is trying to send impressions at a higher rate than the posting thread i To resolve the issue, follow these steps: 1. Increase the size of the impressions queue by updating the `Advanced.ImpressionsQueueSize` parameter. Default is 10k, increasing it to 20k might improve results. -2. Increase the bulk size of the impressions post to Split servers by updating the `Advanced.ImpressionsBulkSize` parameter. Default is 5k. 10k would be a logical next step. -3. Decrease the period at which the SDK sends impressions to the Split servers by adjusting the `TaskPeriods.ImpressionSync` parameter. The default is 30 seconds which is on the low end if you're sending a huge number of impressions. Something along the lines of 5-10 seconds should help. +2. Increase the bulk size of the impressions post to Harness servers by updating the `Advanced.ImpressionsBulkSize` parameter. Default is 5k. 10k would be a logical next step. +3. Decrease the period at which the SDK sends impressions to the Harness servers by adjusting the `TaskPeriods.ImpressionSync` parameter. The default is 30 seconds which is on the low end if you're sending a huge number of impressions. Something along the lines of 5-10 seconds should help. :::note These changes will slightly increase the memory usage of the SDK as well as the network traffic. diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-exception-pkix-path-building-failed.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-exception-pkix-path-building-failed.md index 9537b1d2c7b..0859a65a246 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-exception-pkix-path-building-failed.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-exception-pkix-path-building-failed.md @@ -25,7 +25,7 @@ unable to find valid certification path to requested target ## Root cause -This exception means Java could not download the Split.io certificate, which will prevent the SSL connection to be established between the SDK and Split cloud. +This exception means Java could not download the Split.io certificate, which will prevent the SSL connection to be established between the SDK and Harness FME servers. ## Solution diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-fatal-alert-handshake-failure.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-fatal-alert-handshake-failure.md index d46378df17f..976fcdb218e 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-fatal-alert-handshake-failure.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-fatal-alert-handshake-failure.md @@ -13,7 +13,7 @@ sidebar_position: 12 ## Issue -Using Split Java SDK and JDK 1.6 (JRE 6.x), the following connection error to split.io is thrown: +Using Java SDK and JDK 1.6 (JRE 6.x), the following connection error to split.io is thrown: ``` .RECV TLSv1 ALERT: fatal, handshake_failure diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-how-to-change-log-level.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-how-to-change-log-level.md index 997303255ac..992b59f7ad6 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-how-to-change-log-level.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-how-to-change-log-level.md @@ -11,10 +11,10 @@ sidebar_position: 11

## Question -When integrating Split Java SDK into a framework that uses Log4J, the SDK start logging lot of debugging lines, is it possible to change log level? +When integrating Java SDK into a framework that uses Log4J, the SDK start logging lot of debugging lines, is it possible to change log level? ## Answer -Split Java SDK will pick up the log4j.properties file used for the Java application. +Java SDK will pick up the log4j.properties file used for the Java application. To change the log level to error, add the following line to log4j.properties ``` log4j.logger.split.org.apache = ERROR diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-how-to-deploy-in-aws-lambda.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-how-to-deploy-in-aws-lambda.md index 9c5e6714814..b9f08a2b95e 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-how-to-deploy-in-aws-lambda.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-how-to-deploy-in-aws-lambda.md @@ -40,7 +40,7 @@ import com.amazonaws.services.lambda.runtime.RequestHandler; ```java public class SplitSDK_Sample implements RequestHandler { ``` -4. The code to implement Split SDK should be under the handleRequest function instead of main method remove the code under the main method and paste it under the handleRequest function as below: +4. The code to implement Java SDK should be under the handleRequest function instead of main method remove the code under the main method and paste it under the handleRequest function as below: ```java @Override public String handleRequest(Object input, Context context) { diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-is-there-a-jar-file.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-is-there-a-jar-file.md index bccc12e09f2..a156ab9a746 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-is-there-a-jar-file.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-is-there-a-jar-file.md @@ -1,6 +1,6 @@ --- -title: Is there a JAR file for Split Java SDK? -sidebar_label: Is there a JAR file for Split Java SDK? +title: Is there a JAR file for Java SDK? +sidebar_label: Is there a JAR file for Java SDK? helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 9 @@ -12,11 +12,11 @@ sidebar_position: 9 ## Question -Some Java Frameworks, like ColdFusion, allow third party JAR files to integrate with their code. How can we get a JAR file for Split Java SDK? +Some Java Frameworks, like ColdFusion, allow third party JAR files to integrate with their code. How can we get a JAR file for Java SDK? ## Answer -Split Java SDK uses a Maven repository, which is why no JAR file is needed when using Maven engine to access the SDK and all its dependent libraries. +Java SDK uses a Maven repository, which is why no JAR file is needed when using Maven engine to access the SDK and all its dependent libraries. The JAR file can be downloaded from the Maven repository. Root access URL: https://repo1.maven.org/maven2/io/split/client/java-client/ diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-time-out-error-nosuchmethoderror-google-common.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-time-out-error-nosuchmethoderror-google-common.md index 9fb260c652d..202fd307135 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-time-out-error-nosuchmethoderror-google-common.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/java-sdk-time-out-error-nosuchmethoderror-google-common.md @@ -12,7 +12,7 @@ sidebar_position: 10 ## Issue -Using Split Java SDK within a framework, SDK always times out. Log shows the error below: +Using Java SDK within a framework, SDK always times out. Log shows the error below: ``` 2602 [split-splitFetcher-0] ERROR io.split.engine.experiments.RefreshableSplitFetcher - RefreshableSplitFetcher failed: com.google.common.collect.Multisets.removeOccurrences(Lcom/google/common/collect/Multiset;Ljava/lang/Iterable;)Z 2603 [split-splitFetcher-0] DEBUG io.split.engine.experiments.RefreshableSplitFetcher - Reason: @@ -29,7 +29,7 @@ java.lang.NoSuchMethodError: com.google.common.collect.Multisets.removeOccurrenc ## Root Cause -Split Java SDK uses Google Guava library, the error above will occur if the framework use Google Guava library below 19.0. +Java SDK uses Google Guava library, the error above will occur if the framework use Google Guava library below 19.0. ## Solution Upgrade Google Guava to 19.0 or above version. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/net-sdk-build-error-strongly-named-assembly.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/net-sdk-build-error-strongly-named-assembly.md index 09c2bfcc360..8f7982ff88c 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/net-sdk-build-error-strongly-named-assembly.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/net-sdk-build-error-strongly-named-assembly.md @@ -12,7 +12,7 @@ sidebar_position: 19 ## Problem -In a .NET project that has signing enabled, after adding NET Split SDK, building the project will generate the warning: +In a .NET project that has signing enabled, after adding the FME .NET SDK, building the project will generate the warning: ``` Referenced Assembly 'Splitio, Version=3.4.2.0, Culture=neutral, PublicKeyToken=null' does not have a strong name ``` @@ -24,8 +24,8 @@ Could not load file or assembly 'Splitio, Version=3.4.2.0, Culture=neutral, Publ ## Root cause -Split SDK versions below 3.4.4 have dependency libraries that are packaged without the signed version. In order to generate a signed Split SDK dll, all the dependency dll files must be signed first. +SDK versions below 3.4.4 have dependency libraries that are packaged without the signed version. In order to generate a signed SDK DLL, all the dependency DLL files must be signed first. ## Solution -We have released new Split SDK for .NET with the signed dependencies, please upgrade to latest version. Check our [SDK docs page](https://docs.split.io/docs/net-sdk-overview) for details on how to upgrade. \ No newline at end of file +We have released new FME SDK for .NET with the signed dependencies, please upgrade to latest version. Check our [SDK docs page](https://docs.split.io/docs/net-sdk-overview) for details on how to upgrade. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/net-xamarin-which-api-key.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/net-xamarin-which-api-key.md index 2d2f600f400..9dc216b5ca3 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/net-xamarin-which-api-key.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/net-xamarin-which-api-key.md @@ -14,7 +14,7 @@ sidebar_position: 13 .NET development environment provides Xamarin project which allows .NET code to run on top of a container app in both iOS and Android platforms. -Which API Key to use for .NET or .NET Core Split SDK when developing a Xamarin project? +Which API Key to use for FME .NET or .NET Core SDK when developing a Xamarin project? ## Answer diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-how-to-deploy-in-aws-lambda.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-how-to-deploy-in-aws-lambda.md index b4624638d5a..c8a2cb490c4 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-how-to-deploy-in-aws-lambda.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-how-to-deploy-in-aws-lambda.md @@ -53,7 +53,7 @@ exports.handler = async (event) => { }; ``` -2. Add the dependency libraries for Split SDK, run the command below at the root of your project folder: +2. Add the dependency libraries for the SDK, run the command below at the root of your project folder: ``` npm install --save @splitsoftware/splitio@10.16.0 ``` diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/php-sdk-why-is-php-unable-to-write-impressions-to-redis.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/php-sdk-why-is-php-unable-to-write-impressions-to-redis.md index 066f0c31bc1..d48689a9644 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/php-sdk-why-is-php-unable-to-write-impressions-to-redis.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/php-sdk-why-is-php-unable-to-write-impressions-to-redis.md @@ -36,7 +36,7 @@ $sdkConfig = array( ## Solution -Since the Split PHP SDK uses predis library, we can add the password parameter to the configuration structure: +Since the PHP SDK uses Redis library, we can add the password parameter to the configuration structure: ``` $options = [ 'prefix' => '', diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/python-sdk-error-type-argument-1-must-be-string.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/python-sdk-error-type-argument-1-must-be-string.md index a19cc089a41..9eeb80769e5 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/python-sdk-error-type-argument-1-must-be-string.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/python-sdk-error-type-argument-1-must-be-string.md @@ -32,7 +32,7 @@ type() argument 1 must be string, not unicode ``` ## Root cause -The Python Split SDK requires enum34 library version 1.1.5 or above, if a lower version of enum34 installed (for example 1.0.x), or the environment is forced to use this version, the exception above is thrown when initializing SDK factory object. +The Python SDK requires enum34 library version 1.1.5 or above, if a lower version of enum34 installed (for example 1.0.x), or the environment is forced to use this version, the exception above is thrown when initializing SDK factory object. ## Solution Upgrade enum34 to 1.1.5 or above using pip command. As of this article publishing date, the latest version is 1.1.6. diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-error-uninitialized-constant.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-error-uninitialized-constant.md index dfa18b7ae45..90904651da0 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-error-uninitialized-constant.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-error-uninitialized-constant.md @@ -11,7 +11,7 @@ sidebar_position: 14

## Problem -When using Split Ruby SDK in Windows Platform, initializing the Split factory object causes the error: +When using Ruby SDK in Windows Platform, initializing the SDK factory object causes the error: ``` uninitialized constant error. caused by 'Process::RLIMIT_NOFILE' in lib/net/http/persistent.rb ``` @@ -22,7 +22,7 @@ This issue is related to net-http-persistent 3.0 library in Windows OS. This is ## Solution -The Split SDK works fine with a slightly lower version of net-http-persistent (2.9.4), use the commands below to downgrade it: +The Ruby SDK works fine with a slightly lower version of net-http-persistent (2.9.4), use the commands below to downgrade it: ``` gem uninstall net-http-persistent gem install net-http-persistent -v '2.9.4' diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-example-using-sdk-with-rails-and-sidekiq-service.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-example-using-sdk-with-rails-and-sidekiq-service.md index 73bc9259478..9b1431af587 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-example-using-sdk-with-rails-and-sidekiq-service.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-example-using-sdk-with-rails-and-sidekiq-service.md @@ -1,6 +1,6 @@ --- -title: "Ruby SDK: Example using Split SDK with Rails and Sidekiq service" -sidebar_label: "Ruby SDK: Example using Split SDK with Rails and Sidekiq service" +title: "Ruby SDK: Example using FME SDK with Rails and Sidekiq service" +sidebar_label: "Ruby SDK: Example using FME SDK with Rails and Sidekiq service" helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 2 @@ -10,7 +10,7 @@ sidebar_position: 2

-Example: Basic example to use Split Ruby SDK in Rails and Sidekiq service. +Example: Basic example to use Ruby SDK in Rails and Sidekiq service. Environment: diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-upgrading-from-4-to-5-plus.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-upgrading-from-4-to-5-plus.md index 820d4951a11..3ad45ea02de 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-upgrading-from-4-to-5-plus.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-upgrading-from-4-to-5-plus.md @@ -12,7 +12,7 @@ sidebar_position: 16 ## Issue -Under the hood, Split SDK has a hashing algorithm that divides users across treatments. For example, given a 50/50 split between two treatments (e.g., on and off), the hashing algorithm decides which user is in the on treatment and which one is in the off treatment. +Under the hood, Ruby SDK has a hashing algorithm that divides users across treatments. For example, given a 50/50 split between two treatments (e.g., on and off), the hashing algorithm decides which user is in the on treatment and which one is in the off treatment. Split has used two hashing algorithms: * Legacy Hash (or Algorithm 1). This is simple implementation that is optimized for speed, but suffers from uneven distributions when you have < 100 users. diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-daemon-splitd.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-daemon-splitd.md index e5defabef05..3b772dcc9af 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-daemon-splitd.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-daemon-splitd.md @@ -10,15 +10,15 @@ sidebar_position: 1

-Splitd is a daemon that communicates with the Split backend. It keeps an up-to-date snapshot of the Split rollout plan for a specific Split environment. The rollout plan is accessed by a Split Thin SDK instance (via splitd) to consume feature flags in your code. +Splitd is a daemon that communicates with the Harness servers. It keeps an up-to-date snapshot of the FME definitions for a specific FME environment. The rollout plan is accessed by a FME Thin SDK instance (via splitd) to consume feature flags in your code. -Splitd can be used if you are working in a language that does not have native capability to keep a shared local cache, such as PHP. You can use splitd in combination with a Split Thin SDK (see [Supported Thin SDKs](#supported-thin-sdks)) as an alternative to using Split Synchronizer and Redis with a non-thin Split SDK. Splitd easily scales to high traffic volumes. +Splitd can be used if you are working in a language that does not have native capability to keep a shared local cache, such as PHP. You can use splitd in combination with a FME Thin SDK (see [Supported Thin SDKs](#supported-thin-sdks)) as an alternative to using Split Synchronizer and Redis with a non-thin FME SDK. Splitd easily scales to high traffic volumes. -Splitd is a daemon designed to be deployed in the same host as the application, and works as an offloaded evaluation engine running on a separate process. Splitd relies on the host machine's memory to store the cache and offers an IPC interface via Unix sockets (both stream and sequenced packet sockets supported). Consumer applications (that hold Split Thin SDK instances) connect to splitd and send evaluation requests for Split feature flags as remote procedure calls. Impressions are generated on the daemon and can be backfed to the client to trigger an impression listener. +Splitd is a daemon designed to be deployed in the same host as the application, and works as an offloaded evaluation engine running on a separate process. Splitd relies on the host machine's memory to store the cache and offers an IPC interface via Unix sockets (both stream and sequenced packet sockets supported). Consumer applications (that hold FME Thin SDK instances) connect to splitd and send evaluation requests for FME feature flags as remote procedure calls. Impressions are generated on the daemon and can be backfed to the client to trigger an impression listener. ## Supported Thin SDKs -Splitd currently works with the following Split Thin SDKs: +Splitd currently works with the following FME Thin SDKs: * [Elixir Thin Client SDK](https://help.split.io/hc/en-us/articles/26988707417869) * [PHP Thin Client SDK](https://help.split.io/hc/en-us/articles/18305128673933) @@ -29,9 +29,9 @@ If you are looking for a language that is not listed here, contact the support t The service performs three actions: -* **Fetch targeting rules:** Fetch feature flags and segments from the Split servers. -* **Perform evaluations:** Split Thin SDKs will establish a connection to splitd and send evaluation requests that splitd will service. -* **Post impressions and events:** Impressions (data about a customer's feature flag evaluations) and events will be temporarily stored in the daemon's memory and periodically sent to Split servers. +* **Fetch targeting rules:** Fetch feature flags and segments from the Harness servers. +* **Perform evaluations:** FME Thin SDKs will establish a connection to splitd and send evaluation requests that splitd will service. +* **Post impressions and events:** Impressions (data about a customer's feature flag evaluations) and events will be temporarily stored in the daemon's memory and periodically sent to Harness servers. ### Architecture @@ -51,9 +51,9 @@ The following diagram illustrates a local setup where splitd is on the same serv splitd_shared_instance.drawio.svg

-Since the service relies on interprocess communication (IPC) with thin clients, which happens on the operating system's kernel, the two processes need to be on the same host. The most straightforward way to achieve this is by having both the daemon and the application that bundles the Split Thin SDK in the same server/instance/container. +Since the service relies on interprocess communication (IPC) with thin clients, which happens on the operating system's kernel, the two processes need to be on the same host. The most straightforward way to achieve this is by having both the daemon and the application that bundles the FME Thin SDK in the same server/instance/container. -To set up splitd on the same host as the Split Thin SDK, follow the steps below: +To set up splitd on the same host as the FME Thin SDK, follow the steps below: #### 1. Get a copy of splitd @@ -82,7 +82,7 @@ wget https://raw.githubusercontent.com/splitio/splitd/main/splitd.yaml.tpl \ ``` :::warning[OSX & Unix sockets] -Keep in mind that seqpacket-type sockets only work on the Linux operating system. If you're running a proof of concept on a Mac, you need to set the link type to `unix-stream` in both the daemon and the Split SDK configurations. For more information on socket types, see the [Advanced configuration](#advanced-configuration) section. +Keep in mind that seqpacket-type sockets only work on the Linux operating system. If you're running a proof of concept on a Mac, you need to set the link type to `unix-stream` in both the daemon and the FME SDK configurations. For more information on socket types, see the [Advanced configuration](#advanced-configuration) section. ::: :::info[Configuration file location] @@ -91,7 +91,7 @@ By default, splitd searches for the configuration file at `/etc/splitd.yaml`. Th #### 3. Running splitd -To start splitd, execute the binary `splitd`. This will find and parse the splitd configuration file and start the splitd daemon. Applications using a Split Thin SDK should now be able to connect to the socket created by splitd. +To start splitd, execute the binary `splitd`. This will find and parse the splitd configuration file and start the splitd daemon. Applications using a FME Thin SDK should now be able to connect to the socket created by splitd. When integrating splitd with an application on a server, you will want the daemon to run at system startup and be managed by a background process. The process should automatically restart splitd if it is killed (for example, by the kernel’s memory management). Two popular options are deploying the splitd as a systemd unit or as a supervisord program. These approaches are described below. @@ -329,15 +329,15 @@ link: | **YAML option** | **Environment variable (container-only)** | **Description** | **Default** | **Since** | | --- | --- | --- | --- | --- | -| sdk.apikey | SPLITD_APIKEY | **(required always)** Server-side Split API key | EMPTY | 1.0.0 | +| sdk.apikey | SPLITD_APIKEY | **(required always)** Server-side SDK API key | EMPTY | 1.0.0 | | sdk.streamingEnabled | SPLITD_STREAMING_ENABLED | Whether to enable streaming | `true` | 1.0.0 | | sdk.labelsEnabled | SPLITD_LABELS_ENABLED | Whether to send labels on impressions | `true` | 1.0.0 | | sdk.featureFlags.splitRefreshSeconds | SPLITD_FEATURE_FLAGS_SPLIT_REFRESH_SECS | Refresh rate when operating in polling (seconds) | `30` | 1.0.1 | | sdk.featureFlags.segmentRefreshSeconds | SPLITD_FEATURE_FLAGS_SEGMENT_REFRESH_SECS | Refresh rate for segments when operating in polling (seconds) | `60` | 1.0.1 | | sdk.impressions.mode | SPLITD_IMPRESSIONS_MODE | Impressions handling strategy [`optimized`/`debug`] | `optimized` | 1.0.1 | -| sdk.impressions.refreshRateSeconds | SPLITD_IMPRESSIONS_REFRESH_SECS | How often to flush impressions to Split servers | 1800 | 1.0.1 | +| sdk.impressions.refreshRateSeconds | SPLITD_IMPRESSIONS_REFRESH_SECS | How often to flush impressions to Harness servers | 1800 | 1.0.1 | | sdk.impressions.queueSize | SPLITD_IMPRESSIONS_QUEUE_SIZE | How many impressions (per client) to accumulate before flushing | `8192` | 1.0.1 | -| sdk.events.refreshRateSeconds | SPLITD_EVENTS_REFRESH_SECS | How often to flush events to Split servers | `60` | 1.0.1 | +| sdk.events.refreshRateSeconds | SPLITD_EVENTS_REFRESH_SECS | How often to flush events to Harness servers | `60` | 1.0.1 | | sdk.events.queueSize | SPLITD_EVENTS_QUEUE_SIZE | How many events (per client) to accumulate before flushing | `8192` | 1.0.1 | | sdk.flagSetsFilter | SPLITD_FLAG_SETS_FILTER | This setting allows the Split Synchronizer to synchronize only the feature flags in the specified flag sets. All other flags are not synchronized, resulting in a reduced payload. | empty | 1.2.0 | | sdk.urls.auth | SPLITD_AUTH_URL | Auth Endpoint | `https://auth.split.io/` | 1.0.0 | @@ -359,7 +359,7 @@ link: `splitd` currently supports listenting for connection on two types of unix sockets: `STREAM` and `SEQPACKET`. -Stream-based sockets operate in a similar fashion to TCP-based sockets, without message boundaries and with support for partial reads. Sequenced packet sockets, on the other hand, preserve message boundaries and require the reader to consume the whole package at once (with properly preallocated buffers on the reader side). Since sequenced packet sockets do not require framing/unframing and read with a single syscall, they tend to perform better than stream-based sockets, but they have limited support for large message sizes. With current splitd support for SDK `client.getTreatment()` and `client.getTreatments()` function calls, message size limits are not an issue, but once the manager functionality for querying Split feature flags or dynamic configs become available, it's possible to hit message size limits with the Split Thin SDK `client.getTreatmentsWithConfig()` or `manager.Splits()` function calls. +Stream-based sockets operate in a similar fashion to TCP-based sockets, without message boundaries and with support for partial reads. Sequenced packet sockets, on the other hand, preserve message boundaries and require the reader to consume the whole package at once (with properly preallocated buffers on the reader side). Since sequenced packet sockets do not require framing/unframing and read with a single syscall, they tend to perform better than stream-based sockets, but they have limited support for large message sizes. With current splitd support for SDK `client.getTreatment()` and `client.getTreatments()` function calls, message size limits are not an issue, but once the manager functionality for querying FME feature flags or dynamic configs become available, it's possible to hit message size limits with the FME Thin SDK `client.getTreatmentsWithConfig()` or `manager.Splits()` function calls. For handling large payloads, the following options can be considered: * Use `unix-stream` type sockets. (Note that configuration should be set on both splitd and the consumer application). diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-evaluator.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-evaluator.md index 4015f3f1115..ab7b90b9b6e 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-evaluator.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-evaluator.md @@ -10,7 +10,7 @@ sidebar_position: 2

-Using Split involves using one of our SDKs. The Split team builds and maintains these SDKs for some of the most popular language libraries and the SDKs are available under open source licenses. For languages where there is no native SDK support, Split offers the [Split Evaluator](https://github.com/splitio/split-evaluator), a small service capable of evaluating some or all available features for a given customer via a REST endpoint. +Using Harness FME involves using one of our SDKs. The FME team builds and maintains these SDKs for some of the most popular language libraries and the SDKs are available under open source licenses. For languages where there is no native SDK support, Harness offers the [Split Evaluator](https://github.com/splitio/split-evaluator), a small service capable of evaluating some or all available features for a given customer via a REST endpoint. ## Setup The service is available via Docker or command line and its source code is available at [https://github.com/splitio/split-evaluator]( https://github.com/splitio/split-evaluator). @@ -628,7 +628,7 @@ For more information about how to configure the impression listener, refer to [C ## Multiple environments support -Split Evaluator allows you to set more than one environment. This means that it's possible to evaluate treatments for many Split SDK Keys. To use this feature, the evaluator requires that each Split SDK Key is paired with a custom authorization token (which can be any string) in the environment variable SPLIT_EVALUATOR_ENVIRONMENTS as is shown in the following example: +Split Evaluator allows you to set more than one environment. This means that it's possible to evaluate treatments for many SDK keys. To use this feature, the evaluator requires that each SDK key is paired with a custom authorization token (which can be any string) in the environment variable SPLIT_EVALUATOR_ENVIRONMENTS as is shown in the following example: ```bash SPLIT_EVALUATOR_ENVIRONMENTS='[{"API_KEY":"","AUTH_TOKEN":""},{"API_KEY":"","AUTH_TOKEN":""}]' npm start @@ -674,18 +674,18 @@ The SDK exposes configuration parameters that you can use to optimize SDK perfor | **Configuration** | **Description** | **Default value** | | --- | --- | --- | -| scheduler.featuresRefreshRate | The SDK polls Split servers for changes to feature rollout plans. This parameter controls this polling period in seconds. | 60 | -| scheduler.segmentsRefreshRate | The SDK polls Split servers for changes to segment definitions. This parameter controls this polling period in seconds. | 60 | -| scheduler.impressionsRefreshRate | The SDK sends information on who got what treatment at what time back to Split servers to power analytics. This parameter controls how often this data is sent to Split servers. The parameter is in seconds. | 300 | +| scheduler.featuresRefreshRate | The SDK polls Harness servers for changes to feature rollout plans. This parameter controls this polling period in seconds. | 60 | +| scheduler.segmentsRefreshRate | The SDK polls Harness servers for changes to segment definitions. This parameter controls this polling period in seconds. | 60 | +| scheduler.impressionsRefreshRate | The SDK sends information on who got what treatment at what time back to Harness servers to power analytics. This parameter controls how often this data is sent to Harness servers. The parameter is in seconds. | 300 | | scheduler.impressionsQueueSize | The max amount of impressions we queue. If the queue is full, the SDK flushes the impressions and resets the timer. | 30000 | -| scheduler.eventsPushRate | The SDK sends tracked events to Split servers. This setting controls that flushing rate in seconds. | 60 | +| scheduler.eventsPushRate | The SDK sends tracked events to Harness servers. This setting controls that flushing rate in seconds. | 60 | | scheduler.eventsQueueSize | The max amount of events we queue. If the queue is full, the SDK flushes the events and resets the timer. | 500 | -| scheduler.telemetryRefreshRate | The SDK caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers in seconds. | 3600 seconds (1 hour) | +| scheduler.telemetryRefreshRate | The SDK caches diagnostic data that it periodically sends to Harness servers. This configuration controls how frequently this data is sent back to Harness servers in seconds. | 3600 seconds (1 hour) | | startup.requestTimeoutBeforeReady | Time to wait for a request before the SDK is ready. If this time expires, Node.js SDK tries again `retriesOnFailureBeforeReady` times before notifying its failure to be `ready`. Zero means no timeout. | 15 | | startup.retriesOnFailureBeforeReady | Number of quick retries we do while starting up the SDK. | 1 | | startup.readyTimeout | Maximum amount of time in seconds to wait before notifying a timeout. Zero means no timeout, so no `SDK_READY_TIMED_OUT` event is fired. | 15 | | sync.splitFilters | Filter specific feature flags to be synced and evaluated by the SDK. This is formed by a type string property and a list of string values for the given criteria. Using the types 'bySet' (recommended, flag sets are available in all tiers) or 'byName', pass an array of strings defining the query. If empty or unset, all feature flags are downloaded by the SDK. | [] | -| sync.impressionsMode | This configuration defines how impressions (decisioning events) are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Split. This is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, ALL impressions are queued and sent to Split; this is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. | `OPTIMIZED` | +| sync.impressionsMode | This configuration defines how impressions (decisioning events) are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Harness. This is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Harness FME and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, ALL impressions are queued and sent to Harness; this is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. | `OPTIMIZED` | | debug | Boolean flag or log level string ('ERROR', 'WARN', 'INFO', or 'DEBUG') for activating SDK logs. | false | | streamingEnabled | Boolean flag to enable the streaming service as default synchronization mechanism. In the event of an issue with streaming, the SDK falls back to the polling mechanism. If false, the SDK polls for changes as usual without attempting to use streaming. | true | @@ -728,20 +728,20 @@ For those configurations available on global config and environment variables (f | **Variable** | **Description** | **Default** | | --- | --- | --- | | SPLIT_EVALUATOR_ENVIRONMENTS | String list of environments `"API_KEY":string, "AUTH_TOKEN":string}[]` | - | -| SPLIT_EVALUATOR_API_KEY | Split SDK key to authenticate against Split services. | - | -| SPLIT_EVALUATOR_AUTH_TOKEN | Authentication key used to authenticate every request via the Authorization header. This is **not** a Split SDK key but an arbitrary value defined by the user. | No authentication | +| SPLIT_EVALUATOR_API_KEY | SDK key to authenticate against Harness FME services. | - | +| SPLIT_EVALUATOR_AUTH_TOKEN | Authentication key used to authenticate every request via the Authorization header. This is **not** a SDK key but an arbitrary value defined by the user. | No authentication | | SPLIT_EVALUATOR_GLOBAL_CONFIG | String SDK config for every environment. | - | | SPLIT_EVALUATOR_LOG_LEVEL | Use for setting the log level for service (NONE|INFO|DEBUG|WARN|ERROR). | - | | SPLIT_EVALUATOR_SERVER_PORT | TCP port of the server inside the container. When using in Docker, make sure to match the right side of `-p :` with the value of this variable. | 7548 | | SPLIT_EVALUATOR_IMPRESSION_LISTENER_ENDPOINT | Use it for providing a webhook to send a bulk of Impressions | - | -| SPLIT_EVALUATOR_SPLITS_REFRESH_RATE | The SDK polls Split servers for changes to feature roll-out plans. This parameter controls this polling period in seconds. | 60 | -| SPLIT_EVALUATOR_SEGMENTS_REFRESH_RATE | The SDK polls Split servers for changes to segment definitions. This parameter controls this polling period in seconds. | 60 | -| SPLIT_EVALUATOR_METRICS_POST_RATE | The SDK sends diagnostic metrics to Split servers. This parameters controls this metric flush period in seconds. | 60 | -| SPLIT_EVALUATOR_IMPRESSIONS_POST_RATE | The SDK sends information on who got what treatment at what time back to Split servers to power analytics. This parameter controls how often this data is sent to Split servers. The parameter should be in seconds. | 60 | -| SPLIT_EVALUATOR_EVENTS_POST_RATE | The SDK sends tracked events to Split servers. This setting controls that flushing rate in seconds. | 60 | +| SPLIT_EVALUATOR_SPLITS_REFRESH_RATE | The SDK polls Harness servers for changes to feature roll-out plans. This parameter controls this polling period in seconds. | 60 | +| SPLIT_EVALUATOR_SEGMENTS_REFRESH_RATE | The SDK polls Harness servers for changes to segment definitions. This parameter controls this polling period in seconds. | 60 | +| SPLIT_EVALUATOR_METRICS_POST_RATE | The SDK sends diagnostic metrics to Harness servers. This parameters controls this metric flush period in seconds. | 60 | +| SPLIT_EVALUATOR_IMPRESSIONS_POST_RATE | The SDK sends information on who got what treatment at what time back to Harness servers to power analytics. This parameter controls how often this data is sent to Harness servers. The parameter should be in seconds. | 60 | +| SPLIT_EVALUATOR_EVENTS_POST_RATE | The SDK sends tracked events to Harness servers. This setting controls that flushing rate in seconds. | 60 | | SPLIT_EVALUATOR_EVENTS_QUEUE_SIZE | The max amount of events we queue. If the queue is full, the SDK flushes the events and reset the timer. | 500 | | SPLIT_EVALUATOR_SWAGGER_URL | The url used as base for any Swagger test curl commands. | http://localhost:7548 | -| SPLIT_EVALUATOR_IP_ADDRESSES_ENABLED | Flag to disable IP addresses and host name from being sent to the Split backend. | 'true' | +| SPLIT_EVALUATOR_IP_ADDRESSES_ENABLED | Flag to disable IP addresses and host name from being sent to the Harness servers. | 'true' | ## HTTPS/SSL diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-javascript-synchronizer-tools.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-javascript-synchronizer-tools.md index 2a5d0ce77ef..c7486f6f370 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-javascript-synchronizer-tools.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-javascript-synchronizer-tools.md @@ -17,7 +17,7 @@ import TabItem from '@theme/TabItem'; This guide provides detailed information about our Split JavaScript Sync Tools library. All of our SDKs and libraries are open source. Refer to our [Split JavaScript Sync Tools GitHub repository](https://github.com/splitio/javascript-sync-tools) to see the source code. -Split sync tools is an NPM package that includes the **JavaScript Synchronizer**. This synchronizer coordinates the sending and receiving of data to a remote datastore that all of your processes can share to pull data for the evaluation of treatments. Out of the box, the SDKs pluggable feature allows them to connect to a remote datastore, and so the JavaScript Synchronizer uses same datastore as the cache for your SDKs when it evaluates treatments. It also posts impression and event data and metrics generated by the SDKs back to Split servers, for exposure in the Split user interface or sending it to the data integration of your choice. +Split sync tools is an NPM package that includes the **JavaScript Synchronizer**. This synchronizer coordinates the sending and receiving of data to a remote datastore that all of your processes can share to pull data for the evaluation of treatments. Out of the box, the SDKs pluggable feature allows them to connect to a remote datastore, and so the JavaScript Synchronizer uses same datastore as the cache for your SDKs when it evaluates treatments. It also posts impression and event data and metrics generated by the SDKs back to Harness servers, for exposure in the Harness FME or sending it to the data integration of your choice. ## Language Support @@ -29,10 +29,10 @@ If you're looking for possible polyfill options, for Promise, refer to [es6-prom JavaScript Synchronizer executes as a single run script which performs the following actions: -* **Fetch feature flags:** Retrieve your feature flag definitions from Split servers and write them into the storage. Keep in mind that you can use filters to granularly control which feature flags are synced into the storage. See the [Configuration](#configuration) section for more details. -* **Fetch segments:** Retrieve your set segments lists from Split servers and write them into the storage. -* **Post impressions:** Send to Split servers the stored impressions generated by the SDKs. -* **Post events:** Send to Split servers the stored events generated by the SDKs `.track` method. +* **Fetch feature flags:** Retrieve your feature flag definitions from Harness servers and write them into the storage. Keep in mind that you can use filters to granularly control which feature flags are synced into the storage. See the [Configuration](#configuration) section for more details. +* **Fetch segments:** Retrieve your set segments lists from Harness servers and write them into the storage. +* **Post impressions:** Send to Harness servers the stored impressions generated by the SDKs. +* **Post events:** Send to Harness servers the stored events generated by the SDKs `.track` method. Unlike the [Split Go Synchronizer](https://help.split.io/hc/en-us/articles/360019686092-Split-Synchronizer-Proxy), the JavaScript Synchronizer executes as an script in a compatible JavaScript runtime environment like Node.js. @@ -110,9 +110,9 @@ The JavaScript synchronizer has a number of knobs for configuring performance. E | scheduler.eventsPerPost | Maximum number of events to send per POST request. | 1000 | | scheduler.maxRetries | Maximum number of retry attempts for posting impressions and events. | 3 | | sync.flagSpecVersion | The version of the feature flag definitions to be fetched and stored. | `1.1` | -| sync.splitFilters | List of Split filter objects to granularly control which feature flags are synced into the storage. This is formed by a type string property and a list of string values for the given criteria. Using the types 'bySet' (recommended, flag sets are available in all tiers) or 'byName', pass an array of strings defining the query. If empty or unset, all feature flags are synced. | [] | -| sync.impressionsMode | This configuration defines how impressions extracted from the storage are pre-processed before being sent to Split servers. Supported modes are OPTIMIZED and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Split. In DEBUG mode, ALL impressions are queued and sent to Split. Use DEBUG mode when you want every impression to be logged in the Split user interface when trying to debug your setup. | `OPTIMIZED` | -| sync.requestOptions.agent | A custom Node.js HTTP(S) Agent used to perform the requests to the Split servers. Go to the [Proxy](#proxy) section for details. | undefined | +| sync.splitFilters | List of filter objects to granularly control which feature flags are synced into the storage. This is formed by a type string property and a list of string values for the given criteria. Using the types 'bySet' (recommended, flag sets are available in all tiers) or 'byName', pass an array of strings defining the query. If empty or unset, all feature flags are synced. | [] | +| sync.impressionsMode | This configuration defines how impressions extracted from the storage are pre-processed before being sent to Harness servers. Supported modes are OPTIMIZED and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Harness. In DEBUG mode, ALL impressions are queued and sent to Harness. Use DEBUG mode when you want every impression to be logged in the Harness FME when trying to debug your setup. | `OPTIMIZED` | +| sync.requestOptions.agent | A custom Node.js HTTP(S) Agent used to perform the requests to the Harness servers. Go to the [Proxy](#proxy) section for details. | undefined | | storage.prefix | An optional prefix for your data, to avoid collisions if using the same storage for multiple SDK keys. | `SPLITIO` | | debug | Either a boolean flag or a log level string ('ERROR', 'WARN', 'INFO', or 'DEBUG') for activating Synchronizer logs. | false | @@ -150,7 +150,7 @@ const synchronizer = new Synchronizer({ ## Proxy -If you need to use a network proxy, you can provide a custom [Node.js HTTPS Agent](https://nodejs.org/api/https.html#class-httpsagent) by setting the `sync.requestOptions.agent` configuration variable. The Synchronizer will use this agent to perform requests to Split servers. +If you need to use a network proxy, you can provide a custom [Node.js HTTPS Agent](https://nodejs.org/api/https.html#class-httpsagent) by setting the `sync.requestOptions.agent` configuration variable. The Synchronizer will use this agent to perform requests to Harness servers. diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-proxy.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-proxy.md index f4a9a89599e..782e8adff0c 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-proxy.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-proxy.md @@ -10,9 +10,9 @@ sidebar_position: 3

-The Split Proxy enables you to deploy a service in your own infrastructure that behaves like Split's servers and is used by both server-side and client-side SDKs to synchronize the flags without connecting to Split's actual backend directly. +The Split Proxy enables you to deploy a service in your own infrastructure that behaves like Harness servers and is used by both server-side and client-side SDKs to synchronize the flags without connecting to Harness FME's actual backend directly. -This tool reduces connection latencies from the SDKs to the Split server to the SDKs transparently, and when a single connection is required from a private network to the outside for security reasons. +This tool reduces connection latencies between the SDKs and Harness FME servers, and when a single connection is required from a private network to the outside for security reasons. ### Architecture @@ -39,9 +39,9 @@ docker run --rm --name split-proxy \ ``` :::info[API Keys] -The `SPLIT_PROXY_APIKEY` is the server-side SDK API Key that you can find or create in the Split UI in Admin settings. The Split Proxy uses the `SPLIT_PROXY_APIKEY` to connect to Split servers. +The `SPLIT_PROXY_APIKEY` is the server-side SDK API Key that you can find or create in Admin settings. The Split Proxy uses the `SPLIT_PROXY_APIKEY` to connect to Harness servers. -The `SPLIT_PROXY_CLIENT_APIKEYS` is a list of strings that the Split Proxy will use to authenticate a client. (A Split Proxy client is a client/server-side Split SDK instance that connects to Split Proxy.) The Split Proxy will validate the client by comparing the key the client provides with the strings listed in `SPLIT_PROXY_CLIENT_APIKEYS`. These keys can be any string, generated via any method of your choice. For example, you can generate a GUID or use the string "hello" (e.g. for initial setup and testing the connection). As long as the Proxy client supplies a string that is in the `SPLIT_PROXY_CLIENT_APIKEYS` list, the Proxy will accept the client and forward the request to Split servers. The Split Proxy client (the client/server-side Split SDK instance) will supply a Split Proxy Client API Key in the usual place of the SDK Key. +The `SPLIT_PROXY_CLIENT_APIKEYS` is a list of strings that the Split Proxy will use to authenticate a client. (A Split Proxy client is a client/server-side FME SDK instance that connects to Split Proxy.) The Split Proxy will validate the client by comparing the key the client provides with the strings listed in `SPLIT_PROXY_CLIENT_APIKEYS`. These keys can be any string, generated via any method of your choice. For example, you can generate a GUID or use the string "hello" (e.g. for initial setup and testing the connection). As long as the Proxy client supplies a string that is in the `SPLIT_PROXY_CLIENT_APIKEYS` list, the Proxy will accept the client and forward the request to Harness servers. The Split Proxy client (the client/server-side FME SDK instance) will supply a Split Proxy Client API Key in the usual place of the SDK Key. ::: ### Command line @@ -122,7 +122,7 @@ stdout_logfile_maxbytes = 1MB The Proxy service has several knobs for configuring performance. Each knob is tuned to a reasonable default. However, you can override the default values by changing a `splitio.config.json` file or by setting your customer values as parameters of `-config` in the command line option. In this section, we lay out all the different knobs you can configure for performance, persistent storage, and logging. -The `splitio.config.json` file provided using the `-config` option lets you control how often the synchronizer fetches data from Split servers. You can create a sample JSON file automatically with default values by running the following command: +The `splitio.config.json` file provided using the `-config` option lets you control how often the synchronizer fetches data from Harness servers. You can create a sample JSON file automatically with default values by running the following command: ```bash title="Shell" ./split-proxy -write-default-config "/home/someuser/splitio.config.json" @@ -218,10 +218,10 @@ split-proxy -config "/etc/splitio.config.json" -log-level=info -admin-username=" ``` ### CLI Configuration options and its equivalents in JSON and Environment variables -The following table includes the available command line, JSON, and environment variable options and their descriptions. It specifies configuration options for the Split synchronizer. You can configure the synchronizer using command line arguments, environment variables when you run it as a docker container, and a JSON file when you run it locally. All of these configuration options can be used regardless of the configuration method. +The following table includes the available command line, JSON, and environment variable options and their descriptions. It specifies configuration options for the Split Synchronizer. You can configure the synchronizer using command line arguments, environment variables when you run it as a docker container, and a JSON file when you run it locally. All of these configuration options can be used regardless of the configuration method. :::warning[Split Proxy v5.0 boolean options change] -With the Split synchronizer v5.0.0, the only accepted values for boolean flags are "true" and "false" in lowercase. Values such as "enabled", "on", "yes", or "True" result in an error when you start up. This applies to JSON, CLI arguments, and environment variables. +With the Split Synchronizer v5.0.0, the only accepted values for boolean flags are "true" and "false" in lowercase. Values such as "enabled", "on", "yes", or "True" result in an error when you start up. This applies to JSON, CLI arguments, and environment variables. ::: | **Command line option** | **JSON option** | **Environment variable** (container-only) | **Description** | @@ -247,8 +247,8 @@ With the Split synchronizer v5.0.0, the only accepted values for boolean flags a | impression-listener-queue-size | queueSize | SPLIT_PROXY_IMPRESSION_LISTENER_QUEUE_SIZE | max number of impressions bulks to queue. | | slack-webhook | webhook | SPLIT_PROXY_SLACK_WEBHOOK | slack webhook to post log messages. | | slack-channel | channel | SPLIT_PROXY_SLACK_CHANNEL | slack channel to post log messages. | -| apikey | apikey | SPLIT_PROXY_APIKEY | Split Server-side SDK api-key. | -| ip-address-enabled | ipAddressEnabled | SPLIT_PROXY_IP_ADDRESS_ENABLED | Bundle host's ip address when sending data to Split. | +| apikey | apikey | SPLIT_PROXY_APIKEY | FME server-side SDK API key. | +| ip-address-enabled | ipAddressEnabled | SPLIT_PROXY_IP_ADDRESS_ENABLED | Bundle host's ip address when sending data to Harness FME. | | timeout-ms | timeoutMS | SPLIT_PROXY_TIMEOUT_MS | How long to wait until the synchronizer is ready. | | snapshot | snapshot | SPLIT_PROXY_SNAPSHOT | Snapshot file to use as a starting point. | | force-fresh-startup | forceFreshStartup | SPLIT_PROXY_FORCE_FRESH_STARTUP | Wipe storage before starting the synchronizer | @@ -269,9 +269,9 @@ With the Split synchronizer v5.0.0, the only accepted values for boolean flags a | segment-refresh-rate-ms | segmentRefreshRateMs | SPLIT_PROXY_SEGMENT_REFRESH_RATE_MS | How often to refresh segments. | | streaming-enabled | streamingEnabled | SPLIT_PROXY_STREAMING_ENABLED | Enable/disable streaming functionality. | | http-timeout-ms | httpTimeoutMs | SPLIT_PROXY_HTTP_TIMEOUT_MS | Total http request timeout. | -| impressions-workers | impressionsWorkers | SPLIT_PROXY_IMPRESSIONS_WORKERS | #workers to forward impressions to Split servers. | -| events-workers | eventsWorkers | SPLIT_PROXY_EVENTS_WORKERS | #workers to forward events to Split servers. | -| telemetry-workers | telemetryWorkers | SPLIT_PROXY_TELEMETRY_WORKERS | #workers to forward telemetry to Split servers. | +| impressions-workers | impressionsWorkers | SPLIT_PROXY_IMPRESSIONS_WORKERS | #workers to forward impressions to Harness servers. | +| events-workers | eventsWorkers | SPLIT_PROXY_EVENTS_WORKERS | #workers to forward events to Harness servers. | +| telemetry-workers | telemetryWorkers | SPLIT_PROXY_TELEMETRY_WORKERS | #workers to forward telemetry to Harness servers. | | internal-metrics-rate-ms | internalTelemetryRateMs | SPLIT_PROXY_INTERNAL_METRICS_RATE_MS | How often to send internal metrics. | | dependencies-check-rate-ms | dependenciesCheckRateMs | SPLIT_PROXY_DEPENDENCIES_CHECK_RATE_MS | How often to check dependecies health. | @@ -495,7 +495,7 @@ Returns a binary snapshot file that can be used with the snapshot environment va ### Admin Dashboard -Split-proxy has a web admin user interface out of the box that exposes all available endpoints. Browse to `/admin/dashboard` to see it. +Split Proxy has a web admin user interface out of the box that exposes all available endpoints. Browse to `/admin/dashboard` to see it.

proxy_dashboard_main.png @@ -512,19 +512,19 @@ The dashboard is organized into four sections for easy visualization: - **Healthy Since**: Time passed without errors - **Logged Errors**: Total count of error messages - **SDKs Total Hits**: Total SDKs requests - - **Backend Total Hits**: Total backend requests between split-proxy and Split servers + - **Backend Total Hits**: Total backend requests between split-proxy and Harness servers - **Cached Feature flags**: Number of feature flags cached in memory - **Cached Segments**: Number of segments cached in memory - - **SDK Server**: displays the status of Split server for SDK - - **Events Server**: displays the status of Split server for Events - - **Streaming Server**: displays the status of Split streaming service - - **Auth Server**: displays the status of Split server for initial streaming authentication - - **Telemetry Server**: displays the status of Split server for telemetry capturing + - **SDK Server**: displays the status of server for SDK + - **Events Server**: displays the status of server for Events + - **Streaming Server**: displays the status of streaming service + - **Auth Server**: displays the status of server for initial streaming authentication + - **Telemetry Server**: displays the status of server for telemetry capturing - **Storage**: (only Sync mode) displays the status of the storage - **Sync**: displays the status of the Proxy - **Last Errors Log**: List of the last 10 error messages * **SDK stats**: Metrics numbers and a latency graph, measured between SDKs requests integration and proxy -* **Split stats**: Metrics numbers and a latency graph, measured between proxy requests integration with Split servers +* **Split stats**: Metrics numbers and a latency graph, measured between proxy requests integration with Harness servers * **Data inspector**: Cached data showing feature flags and segments; filters to find keys and feature flag definitions

proxy_dashboard_data.png diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-synchronizer.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-synchronizer.md index dde1502cab4..33e6b6cf249 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-synchronizer.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/optional-infra/split-synchronizer.md @@ -13,9 +13,9 @@ sidebar_position: 4 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; -By default, Split’s SDKs keep segment and feature flag data synchronized as users navigate across disparate systems, treatments, and conditions. However, some languages, do not have a native capability to keep a shared local cache of this data to properly serve treatments. For these cases, we built the Split Synchronizer service. +By default, FME's SDKs keep segment and feature flag data synchronized as users navigate across disparate systems, treatments, and conditions. However, some languages, do not have a native capability to keep a shared local cache of this data to properly serve treatments. For these cases, we built the Split Synchronizer service. -This tool coordinates the sending and receiving of data to a remote datastore that all of your processes can share to pull data for the evaluation of treatments. Out of the box, Split supports Redis as a remote datastore, and so the Split Synchronizer uses Redis as the cache for your SDKs when evaluating treatments. It also posts impression and event data and metrics generated by the SDKs back to Split’s servers, for exposure in the user interface or sending to the data integration of your choice. The Synchronizer service runs as a standalone process in dedicated or shared servers and it does not affect the performance of your code, or Split’s SDKs. +This tool coordinates the sending and receiving of data to a remote datastore that all of your processes can share to pull data for the evaluation of treatments. Out of the box, FME supports Redis as a remote datastore, and so the Split Synchronizer uses Redis as the cache for your SDKs when evaluating treatments. It also posts impression and event data and metrics generated by the SDKs back to Harness servers, for exposure in the user interface or sending to the data integration of your choice. The Synchronizer service runs as a standalone process in dedicated or shared servers and it does not affect the performance of your code, or FME's SDKs. :::info[Split Synchronizer version 5.0 available!] Since version 5.0.0 of the split-synchronizer, there's only one operation mode. What was once `proxy mode` is now a separate tool called [**Split proxy**](https://help.split.io/hc/en-us/articles/4415960499213-Split-Proxy). This version includes a more performant way to evict impressions & events from redis that allows customers to handle much greater volumes of data, while protecting your flags from being evicted if the redis instance runs low on memory. @@ -31,12 +31,12 @@ Java 4.4.0 ::: :::warning -If you are upgrading from Split synchronizer version 4.x or below to 5.x, some of the configuration and environment parameter names have been changed. Refer to the Configuration section and modify the parameters names accordingly. +If you are upgrading from Split Synchronizer version 4.x or below to 5.x, some of the configuration and environment parameter names have been changed. Refer to the Configuration section and modify the parameters names accordingly. ::: ## Supported SDKs -The Split Synchronizer works with most of the languages that Split supports. +The Split Synchronizer works with most of the languages that FME supports. * [PHP SDK](https://help.split.io/hc/en-us/articles/360020350372) * [Python SDK](https://help.split.io/hc/en-us/articles/360020359652) @@ -54,18 +54,18 @@ If you are looking for a language that is not listed here, contact the support t * **Fetch feature flags:** Retrieve the feature flag definitions. * **Fetch segments:** Retrieve your set segments lists. -* **Post impressions:** Send to Split servers the generated impressions by the SDK. -* **Post telemetry:** Send to Split servers different metrics of the SDK, such as latencies. -* **Post events:** Send to Split servers the generated events by the SDK `.track` method. +* **Post impressions:** Send to Harness servers the generated impressions by the SDK. +* **Post telemetry:** Send to Harness servers different metrics of the SDK, such as latencies. +* **Post events:** Send to Harness servers the generated events by the SDK `.track` method. :::info[Split-Sync v5.0.0 pipelined data eviction] -Starting with split-sync v5.0.0, we've introduced a new approach to impressions and events eviction. This replaces the previous approach of periodically fetching & posting impressions and events. Our new approch feature flags this task in 3 parts, a thread dedicated to fetching data from redis and placing it in a buffer, N threads (where N is derived from the number of available CPU cores) dedicated to parsing, formatting the data and placing it in a second buffer, and N (configurable) threads that pick the data and post it to Split servers. The result is a significant increase in throughput, that will better suit customers which operate on big volumes of data. +Starting with split-sync v5.0.0, we've introduced a new approach to impressions and events eviction. This replaces the previous approach of periodically fetching & posting impressions and events. Our new approch feature flags this task in 3 parts, a thread dedicated to fetching data from redis and placing it in a buffer, N threads (where N is derived from the number of available CPU cores) dedicated to parsing, formatting the data and placing it in a second buffer, and N (configurable) threads that pick the data and post it to Harness servers. The result is a significant increase in throughput, that will better suit customers which operate on big volumes of data. ::: ### Architecture

- Split synchronizer architecture diagram + Split Synchronizer architecture diagram

## Setup @@ -231,7 +231,7 @@ stdout_logfile_maxbytes = 1MB The Synchronizer service has a number of knobs for configuring performance. Each knob is tuned to a reasonable default, however, you can override the default values by changing a `splitio.config.json` file or by setting your customer values as parameters of `-config` in the command line option. In this section, we lay out all the different knobs you can configure for performance, Redis, and logging. -The `splitio.config.json` file provided via the `-config` option lets you control how often the synchronizer fetches data from Split servers. You can create a sample JSON file automatically with default values by running this command. +The `splitio.config.json` file provided via the `-config` option lets you control how often the synchronizer fetches data from Harness servers. You can create a sample JSON file automatically with default values by running this command. ```bash ./split-sync -write-default-config "/home/someuser/splitio.config.json" @@ -532,9 +532,9 @@ Remember to set the right path as the `-config` parameter. All the options available in the JSON file are also included as command line options. Run the command followed by the `-help` option to see more details, or just keep reading this documentation page. ::: -### Methods to configure the Split synchronizer +### Methods to configure the Split Synchronizer -You can configure the Split synchronizer service using the command line or by directly editing the above mentioned **JSON** configuration file. +You can configure the Split Synchronizer service using the command line or by directly editing the above mentioned **JSON** configuration file. :::info[Config values priority] All config values are set with a default value that you can see in the example **JSON** file above. You can overwrite the default value from the JSON config file, and you can overwrite the JSON config file from the command line. See a sample below for how to do that via command line. @@ -575,7 +575,7 @@ In order to reduce the issues because of typos and confusion due to "multiple wo | slack-webhook | webhook | SPLIT_SYNC_SLACK_WEBHOOK | slack webhook to post log messages | | slack-channel | channel | SPLIT_SYNC_SLACK_CHANNEL | slack channel to post log messages | | apikey | apikey | SPLIT_SYNC_APIKEY | Split Server-side SDK api-key | -| ip-address-enabled | ipAddressEnabled | SPLIT_SYNC_IP_ADDRESS_ENABLED | Bundle host's ip address when sending data to Split | +| ip-address-enabled | ipAddressEnabled | SPLIT_SYNC_IP_ADDRESS_ENABLED | Bundle host's ip address when sending data to Harness FME | | timeout-ms | timeoutMS | SPLIT_SYNC_TIMEOUT_MS | How long to wait until the synchronizer is ready | | snapshot | snapshot | SPLIT_SYNC_SNAPSHOT | Snapshot file to use as a starting point | | force-fresh-startup | forceFreshStartup | SPLIT_SYNC_FORCE_FRESH_STARTUP | Wipe storage before starting the synchronizer | @@ -624,7 +624,7 @@ In order to reduce the issues because of typos and confusion due to "multiple wo | redis-tls-client-certificate | tlsClientCertificate | SPLIT_SYNC_REDIS_TLS_CLIENT_CERTIFICATE | Client certificate signed by a known CA | | redis-tls-client-key | tlsClientKey | SPLIT_SYNC_REDIS_TLS_CLIENT_KEY | Client private key matching the certificate. | | storage-check-rate-ms | storageCheckRateMs | SPLIT_SYNC_STORAGE_CHECK_RATE_MS | How often to check storage health | -| flag-sets-filter | flagSetsFilter | SPLIT_SYNC_FLAG_SETS_FILTER | This setting allows the split synchronizer to only synchronize the feature flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the split synchronizer instance, bringing all the benefits from a reduced payload. | +| flag-sets-filter | flagSetsFilter | SPLIT_SYNC_FLAG_SETS_FILTER | This setting allows the Split Synchronizer to only synchronize the feature flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the Split Synchronizer instance, bringing all the benefits from a reduced payload. | ## Listener @@ -880,7 +880,7 @@ The dashboard is organized in four sections for ease of visualization: - *Healthy Since:* Time passed without errors - *Logged Errors:* Total count of error messages - *SDKs Total Hits:* Total SDKs requests - - *Backend Total Hits:* Total backend requests between split-sync and Split servers + - *Backend Total Hits:* Total backend requests between split-sync and Harness servers - *Cached Feature flags:* Number of feature flags cached in memory - *Cached Segments:* Number of segments cached in memory - *Impressions Queue Size*: shows the total amount of Impressions stored in Redis (only Producer Mode). diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/sdk-overview.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/sdk-overview.md index 69a298ec67f..c1d5e536cea 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/sdk-overview.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/sdk-overview.md @@ -10,29 +10,29 @@ sidebar_position: 1

-When you integrate Split SDKs, consider the following to make sure that you have the correct set up depending on your use case, customers, security considerations, and architecture. +When you integrate FME SDKs, consider the following to make sure that you have the correct set up depending on your use case, customers, security considerations, and architecture. -* **Understand Split's architecture**. Split's SDKs were built to be scalable, reliable, fast, independent, and secure. +* **Understand Harness FME architecture**. FME SDKs were built to be scalable, reliable, fast, independent, and secure. * **Determine which SDK type**. Depending on your use case and your application stack, you may need a server-side or client-side SDK. * **Understand security considerations**. Client- and server-side SDKs have different security considerations when managing and targeting using your customers' PII. * **Determine which API key**. In Split, there are three types of keys with each providing different levels of access to Split's API. Understand what each key provides access to and when to use each API key. -* **Determine which SDK language**. Split supports serveral SDKs across various languages. With Split, you can use multiple SDKs if your product is comprised of applications written in multiple languages. -* **Determine if you need to use the Split Synchronizer & Proxy**. By default, Split's SDKs keep segment and feature flag definitions synchronized as users navigate across disparate systems, treatments, and conditions. However, some languages do not have a native capability to keep a shared local cache of this data to properly serve treatments. For these cases, we built the Split Synchronizer. To learn more, refer to the [Split Synchronizer and Proxy guide](https://help.split.io/hc/en-us/articles/360019686092-Split-synchronizer-proxy). +* **Determine which SDK language**. FME supports serveral SDKs across various languages. With Split, you can use multiple SDKs if your product is comprised of applications written in multiple languages. +* **Determine if you need to use the Split Synchronizer & Proxy**. By default, FME SDKs keep segment and feature flag definitions synchronized as users navigate across disparate systems, treatments, and conditions. However, some languages do not have a native capability to keep a shared local cache of this data to properly serve treatments. For these cases, we built the Split Synchronizer. To learn more, refer to the [Split Synchronizer and Proxy guide](https://help.split.io/hc/en-us/articles/360019686092-Split-synchronizer-proxy). ## Streaming architecture overview -Split's SDKs were built to be scalable, reliable, fast, independent, and secure. +FME SDKs were built to be scalable, reliable, fast, independent, and secure. -* **Scalable**. Split is currently serving more than 50 billion Split feature flag evaluations per day. If you've shopped online, purchased an airline ticket, or received a text message from service provider, you've likely experienced Split. -* **Reliable and fast**. Our scalable and flexible architecture uses a dual-layer CDN to serve feature flags anywhere in the world in less than 200 ms. In most instances, Split rollout plan updates are streamed to Split's SDKs, which takes a fraction of a second. In less than 10% of cases, for very large feature flag definitions (or large dynamic configs) or segment updates with a large number of key changes, a notification of the change is streamed and the changes are retrieved by an API fetch request. Our SDKs store the Split rollout plan locally to serve feature flags without a network call and without interruption in the event of a network outage. +* **Scalable**. Split is currently serving more than 50 billion feature flag evaluations per day. If you've shopped online, purchased an airline ticket, or received a text message from service provider, you've likely experienced Split. +* **Reliable and fast**. Our scalable and flexible architecture uses a dual-layer CDN to serve feature flags anywhere in the world in less than 200 ms. In most instances, Split rollout plan updates are streamed to FME SDKs, which takes a fraction of a second. In less than 10% of cases, for very large feature flag definitions (or large dynamic configs) or segment updates with a large number of key changes, a notification of the change is streamed and the changes are retrieved by an API fetch request. Our SDKs store the Split rollout plan locally to serve feature flags without a network call and without interruption in the event of a network outage. * **Independent with no Split dependency**. Split ships the evaluation engine to each SDK creating a weak dependency with Split's backend and increasing both speed and reliability. There are no network calls to Split to decide a user's treatment. * **Secure with no PII required**. No customer data needs to be sent through the cloud to Split. Use customer data in your feature flag evaluations without exposing this data to third parties. ## Streaming versus polling -Split updates can be streamed to Split's SDKs sub second or retrieved on configurable polling intervals. +FME updates can be streamed to FME SDKs sub second or retrieved on configurable polling intervals. -When streaming, Split utilizes [server-sent events (SSE)](https://www.w3schools.com/html/html5_serversentevents.asp) to notify Split’s SDKs when a feature flag definition is updated, a segment definition is updated, or a feature flag is killed. For feature flag and segment definition updates, the Split SDK reacts to this notification and fetches the latest feature flag definition or segment definition. When a feature flag is killed, the notification triggers a kill event immediately. When the SDK is running with streaming enabled, your updates take effect in milliseconds. +When streaming, Harness FME utilizes [server-sent events (SSE)](https://www.w3schools.com/html/html5_serversentevents.asp) to notify FME SDKs when a feature flag definition is updated, a segment definition is updated, or a feature flag is killed. For feature flag and segment definition updates, the SDK reacts to this notification and fetches the latest feature flag definition or segment definition. When a feature flag is killed, the notification triggers a kill event immediately. When the SDK is running with streaming enabled, your updates take effect in milliseconds. Enable streaming when it is important to: @@ -126,11 +126,11 @@ For languages with no native SDK support, Split offers the Split Evaluator, a sm ## Synchronizer service -By default, Split's SDKs keep segment and feature flag definitions synchronized in an in-memory cache for speed at evaluating feature flags. However, some languages do not have a native capability to keep a shared local cache of this data to properly serve treatments. For these cases, we built Split Synchronizer to maintain an external cache like Redis. To learn more, read about [Split Synchronizer](https://help.split.io/hc/en-us/articles/360019686092-Split-Synchronizer). +By default, FME SDKs keep segment and feature flag definitions synchronized in an in-memory cache for speed at evaluating feature flags. However, some languages do not have a native capability to keep a shared local cache of this data to properly serve treatments. For these cases, we built Split Synchronizer to maintain an external cache like Redis. To learn more, read about [Split Synchronizer](https://help.split.io/hc/en-us/articles/360019686092-Split-Synchronizer). ## Proxy service -Split Proxy enables you to deploy a service in your own infrastructure that behaves like Split's servers and is used by both server-side and client-side SDKs to synchronize the flags without directly connecting to Split's backend. +Split Proxy enables you to deploy a service in your own infrastructure that behaves like Harness servers and is used by both server-side and client-side SDKs to synchronize the flags without directly connecting to Split's backend. This tool reduces connection latencies between the SDKs and the Split server, and can be used when a single connection is required from a private network to the outside for security reasons. To learn more, read about [Split Proxy](https://help.split.io/hc/en-us/articles/4415960499213-Split-Proxy). diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/sdk-validation-checklist.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/sdk-validation-checklist.md index 430f35dc7d6..55dd69b5b94 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/sdk-validation-checklist.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/sdk-validation-checklist.md @@ -10,16 +10,16 @@ helpdocs_is_published: true

-The SDK validation checklist helps you ensure that the SDK is implemented according to Split’s best practices. This document describes the guidelines for incorporating the Split SDK into your software application in all supported languages. The main purpose is to define the general guidelines, checks, and validations that can be useful for developers and software architects to avoid common mistakes or oversights and to ensure optimal performance of the Split SDK. This guide covers recommendations in the following areas: +The SDK validation checklist helps you ensure that the SDK is implemented according to FME best practices. This document describes the guidelines for incorporating the SDK into your software application in all supported languages. The main purpose is to define the general guidelines, checks, and validations that can be useful for developers and software architects to avoid common mistakes or oversights and to ensure optimal performance of the SDK. This guide covers recommendations in the following areas: * Architectural design principles * Safety checks for prevention of race conditions * Taking advantage of helpful Split features * Configuration validation exercises -These areas each reflect best practices that come from our own experience at Split using the Split SDK, and the experiences of customers like you. In addition, they also convey an understanding of how Split SDK works beneath the surface. +These areas each reflect best practices that come from our own experience at Split using the SDK, and the experiences of customers like you. In addition, they also convey an understanding of how SDK works beneath the surface. -You can use or adapt them to your needs. The primary objectives are to ensure resource optimization, maximum application responsiveness, appropriate security enforcements, and proactive issue detection in your project, team, organization, or company source code working with the Split SDK. +You can use or adapt them to your needs. The primary objectives are to ensure resource optimization, maximum application responsiveness, appropriate security enforcements, and proactive issue detection in your project, team, organization, or company source code working with the SDK. ## All SDKs @@ -27,7 +27,7 @@ The following validation considerations are relevant for all of Split’s SDKs. * **Ensure that the SDK is implemented in a singleton pattern.** Using the SDK as a singleton ensures that the minimum number of threads are used to serve your application. If you don’t, you can overload your infrastructure with unnecessary network traffic and use up far more application threads than is required. Use multiple clients on the client side from a single factory if you need to get treatments for multiple different traffic type ids. -* **Ensure that the SDK is blocked until it signals it’s ready.** All Split SDKs have a method that blocks the thread until the SDK is ready with feature flag and segment definitions. Calling getTreatment before the SDK is ready gives CONTROL treatments. +* **Ensure that the SDK is blocked until it signals it’s ready.** All FME SDKs have a method that blocks the thread until the SDK is ready with feature flag and segment definitions. Calling getTreatment before the SDK is ready gives CONTROL treatments. * **Run the SDK with DEBUG enabled and evaluate any errors or warning messages that are thrown.** Pay attention specifically to errors or warnings related to multiple factories, missing event listeners, or other incorrect factory and client configuration. **Note: Run with debug enabled for only a few minutes.** @@ -35,9 +35,9 @@ The following validation considerations are relevant for all of Split’s SDKs. * **Validate SDK Versions are up to date.** Review the SDK tab on the Account usage data page. Ensure that the SDKs are up to date, or, at the minimum, they are on the same major version. It is helpful to establish and document a regular SDK update cadence, such as quarterly or biannually. Check the SDK CHANGES.txt on github for any SDKs you are using to see if anything may be relevant to your usage of Split. -* **Evaluate if you can take advantage of the SDK .destroy() method.** The .destroy() method of the SDK flushes all stored unpublished events and impressions. This is primarily advantageous for the client side SDKs where you have parts of the user journey that explicitly end their session. On the browser, .destroy() returns a promise. If it’s resolved, then you can be sure that all data is pushed to Split. On the server side it also may be useful in the event that you need to shutdown a service running the Split SDK. Calling .destroy() ensures that data is posted back to Split. +* **Evaluate if you can take advantage of the SDK .destroy() method.** The .destroy() method of the SDK flushes all stored unpublished events and impressions. This is primarily advantageous for the client side SDKs where you have parts of the user journey that explicitly end their session. On the browser, .destroy() returns a promise. If it’s resolved, then you can be sure that all data is pushed to Harness. On the server side it also may be useful in the event that you need to shutdown a service running the FME SDK. Calling .destroy() ensures that data is posted back to Harness. -* **Validate 1 minute of impressions (and events) on the Split live tail.** Enable the Query for about a minute and ensure that the number of impressions received by Split is about what you’d expect from SDK activity. +* **Validate 1 minute of impressions (and events) on the Split live tail.** Enable the Query for about a minute and ensure that the number of impressions received by Harness is about what you’d expect from SDK activity. * **If you have events coming in, validate them with a similar approach.** Ensure that events coming in have the event properties that you would expect them to have. @@ -63,7 +63,7 @@ The following items are specific to browser-based SDKs. The following items are specific to the mobile SDKs. -* **Ensure that the SDK background syncing is enabled if desired.** Mobile SDKs have the synchronizeInBackground configuration setting that allows them to synchronize to the Split cloud while in the background. By default, this is disabled. +* **Ensure that the SDK background syncing is enabled if desired.** Mobile SDKs have the synchronizeInBackground configuration setting that allows them to synchronize to the Harness FME servers while in the background. By default, this is disabled. ## All Client-side SDKs (including iOS, React, JS, etc.) @@ -74,11 +74,11 @@ The following items are specific to all client-side SDKs. This includes mobile- * SDK_READY_TIMED_OUT. When this event fires, it doesn't mean the SDK initialization is interrupted. SDK_READY may still fire at a later time if or when the SDK finishes downloading the necessary information from the servers. This may happen with slow connections or environments which have many feature flags, segments, or dynamic configurations. * SDK_UPDATE. This event fires whenever a feature flag or segment is changed. Use this if you want to reload your app every time you make a change in the user interface. -* **Evaluate if you need to change the flush rate.** The SDK posts impressions on frequency based on the parameter scheduler.impressionsRefreshRate. By default, the parameter is set to 60 seconds in the browser and 30 minutes in the mobile SDKs. This means after the getTreatment function is called, impressions get posted back to the Split cloud after that length of time. +* **Evaluate if you need to change the flush rate.** The SDK posts impressions on frequency based on the parameter scheduler.impressionsRefreshRate. By default, the parameter is set to 60 seconds in the browser and 30 minutes in the mobile SDKs. This means after the getTreatment function is called, impressions get posted back to the Harness FME servers after that length of time. On mobile devices, if the user stays in the app for less than that amount of time, the impressions stay in the SDK cache. However, they are not posted as the posting thread has not run yet. The next time a user opens the app, the impressions are posted but this can be a few days later. - For browsers, the JS SDKs use the beacon API to post results back to the Split cloud when the page is no longer visible. + For browsers, the JS SDKs use the beacon API to post results back to the Harness FME servers when the page is no longer visible. For experimentation, it is desired to have the results up to date. It is recommended to set the parameter scheduler.impressionsRefreshRate to a value less than the average time the user stays on the app. @@ -86,8 +86,8 @@ The following items are specific to all client-side SDKs. This includes mobile- The following items are specific to server-side SDKs. -* **Evaluate your traffic needs.** You may need to change the impressionsRefreshrate. The SDK has threads that sync the Split information from Split cloud to the cache, and posts all impressions and events created in the cache. Make sure the SDK can handle the incoming impressions load because the SDK drops impressions if the cap is reached in the impressionsQueue and impressions can’t be evicted. +* **Evaluate your traffic needs.** You may need to change the impressionsRefreshrate. The SDK has threads that sync the Split information from Harness FME servers to the cache, and posts all impressions and events created in the cache. Make sure the SDK can handle the incoming impressions load because the SDK drops impressions if the cap is reached in the impressionsQueue and impressions can’t be evicted. The SDK has parameters to control the run frequency for these threads. We recommend to estimate the highest number of impressions created at peak time from incoming user sessions and divide that by the number of app servers that have the SDK to estimate the number of treatments per minute each SDK generates. Roughly, the SDK’s default impressionsQueue can handle 2000 treatments per minute. If the peak time generates higher impressions, we can reduce the value of scheduler.impressionsRefreshRate by half (for example, from 60 to 30 seconds). -**Note This traffic sizing is for pushing data back to Split cloud. Even if the impressionsQueue is full and drops impressions, serving treatments are not affected.** \ No newline at end of file +**Note This traffic sizing is for pushing data back to Harness FME servers. Even if the impressionsQueue is full and drops impressions, serving treatments are not affected.** \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/sdk-versioning-policy.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/sdk-versioning-policy.md index 9cd930d19bd..1e8a681b573 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/sdk-versioning-policy.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/sdk-versioning-policy.md @@ -40,4 +40,4 @@ Bug fixes that preserve compatibility are released as a patch version. Depending ## Version support -Split supports and patches prior major releases for up to 12 months following the version release date. If 12 months has elapsed, support and Split’s SDK engineering team may ask you to first upgrade to the current major release before attempting to patch old versions. \ No newline at end of file +FME supports and patches prior major releases for up to 12 months following the version release date. If 12 months has elapsed, support and Split’s SDK engineering team may ask you to first upgrade to the current major release before attempting to patch old versions. \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/troubleshooting.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/troubleshooting.md index 94a8d6f6fc9..b3b6e9d729a 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/troubleshooting.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/sdk-overview/troubleshooting.md @@ -10,29 +10,29 @@ helpdocs_is_published: true

-When you integrate Split SDKs, consider the following to make sure that you have the correct set up depending on your use case, customers, security considerations, and architecture. +When you integrate FME SDKs, consider the following to make sure that you have the correct set up depending on your use case, customers, security considerations, and architecture. -* **Understand Split's architecture**. Split's SDKs were built to be scalable, reliable, fast, independent, and secure. +* **Understand Harness FME architecture**. FME SDKs were built to be scalable, reliable, fast, independent, and secure. * **Determine which SDK type**. Depending on your use case and your application stack, you may need a server-side or client-side SDK. * **Understand security considerations**. Client- and server-side SDKs have different security considerations when managing and targeting using your customers' PII. * **Determine which API key**. In Split, there are three types of keys with each providing different levels of access to Split's API. Understand what each key provides access to and when to use each API key. -* **Determine which SDK language**. Split supports serveral SDKs across various languages. With Split, you can use multiple SDKs if your product is comprised of applications written in multiple languages. +* **Determine which SDK language**. FME supports serveral SDKs across various languages. With Split, you can use multiple SDKs if your product is comprised of applications written in multiple languages. * **Determine if you need to use the Split Synchronizer & Proxy**. By default, Split's SDKs keep segment and feature flag definitions synchronized as users navigate across disparate systems, treatments, and conditions. However, some languages do not have a native capability to keep a shared local cache of this data to properly serve treatments. For these cases, we built the Split Synchronizer. To learn more, refer to the [Split Synchronizer and Proxy guide](https://help.split.io/hc/en-us/articles/360019686092-Split-synchronizer-proxy). ## Streaming architecture overview Split's SDKs were built to be scalable, reliable, fast, independent, and secure. -* **Scalable**. Split is currently serving more than 50 billion Split feature flag evaluations per day. If you've shopped online, purchased an airline ticket, or received a text message from service provider, you've likely experienced Split. +* **Scalable**. Split is currently serving more than 50 billion feature flag evaluations per day. If you've shopped online, purchased an airline ticket, or received a text message from service provider, you've likely experienced Split. * **Reliable and fast**. Our scalable and flexible architecture uses a dual-layer CDN to serve feature flags anywhere in the world in less than 200 ms. In most instances, Split rollout plan updates are streamed to Split's SDKs, which takes a fraction of a second. In less than 10% of cases, for very large feature flag definitions (or large dynamic configs) or segment updates with a large number of key changes, a notification of the change is streamed and the changes are retrieved by an API fetch request. Our SDKs store the Split rollout plan locally to serve feature flags without a network call and without interruption in the event of a network outage. * **Independent with no Split dependency**. Split ships the evaluation engine to each SDK creating a weak dependency with Split's backend and increasing both speed and reliability. There are no network calls to Split to decide a user's treatment. * **Secure with no PII required**. No customer data needs to be sent through the cloud to Split. Use customer data in your feature flag evaluations without exposing this data to third parties. ## Streaming versus polling -Split updates can be streamed to Split's SDKs sub second or retrieved on configurable polling intervals. +FME updates can be streamed to FME SDKs sub second or retrieved on configurable polling intervals. -When streaming, Split utilizes [server-sent events (SSE)](https://www.w3schools.com/html/html5_serversentevents.asp) to notify Split’s SDKs when a feature flag definition is updated, a segment definition is updated, or a feature flag is killed. For feature flag and segment definition updates, the Split SDK reacts to this notification and fetches the latest feature flag definition or segment definition. When a feature flag is killed, the notification triggers a kill event immediately. When the SDK is running with streaming enabled, your updates take effect in milliseconds. +When streaming, Split utilizes [server-sent events (SSE)](https://www.w3schools.com/html/html5_serversentevents.asp) to notify Split’s SDKs when a feature flag definition is updated, a segment definition is updated, or a feature flag is killed. For feature flag and segment definition updates, the FME SDK reacts to this notification and fetches the latest feature flag definition or segment definition. When a feature flag is killed, the notification triggers a kill event immediately. When the SDK is running with streaming enabled, your updates take effect in milliseconds. Enable streaming when it is important to: @@ -130,7 +130,7 @@ By default, Split's SDKs keep segment and feature flag definitions synchronized ## Proxy service -Split Proxy enables you to deploy a service in your own infrastructure that behaves like Split's servers and is used by both server-side and client-side SDKs to synchronize the flags without directly connecting to Split's backend. +Split Proxy enables you to deploy a service in your own infrastructure that behaves like Harness servers and is used by both server-side and client-side SDKs to synchronize the flags without directly connecting to Split's backend. This tool reduces connection latencies between the SDKs and the Split server, and can be used when a single connection is required from a private network to the outside for security reasons. To learn more, read about [Split Proxy](https://help.split.io/hc/en-us/articles/4415960499213-Split-Proxy). diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/go-app.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/go-app.md index 856ff996ba5..a2fb9a28b88 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/go-app.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/go-app.md @@ -1,6 +1,6 @@ --- -title: Go app project using Split SDK example -sidebar_label: Go app project using Split SDK example +title: Go app project using FME SDK example +sidebar_label: Go app project using FME SDK example helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 7 @@ -10,4 +10,4 @@ sidebar_position: 7

-[Go app project using Split SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/Go-SDK) \ No newline at end of file +[Go app project using FME SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/Go-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/java-app.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/java-app.md index 16b78cdc33b..d8e765b96bf 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/java-app.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/java-app.md @@ -1,6 +1,6 @@ --- -title: Java app project using Split SDK example -sidebar_label: Java app project using Split SDK example +title: Java app project using FME SDK example +sidebar_label: Java app project using FME SDK example helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 12 @@ -10,4 +10,4 @@ sidebar_position: 12

-[Java app project using Split SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/Java-SDK) \ No newline at end of file +[Java app project using FME SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/Java-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/net-core-csharp-app.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/net-core-csharp-app.md index 2f106bfbab1..f0703d8283f 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/net-core-csharp-app.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/net-core-csharp-app.md @@ -1,6 +1,6 @@ --- -title: .NET Core C# app project using Split SDK example -sidebar_label: .NET Core C# app project using Split SDK example +title: .NET Core C# app project using FME SDK example +sidebar_label: .NET Core C# app project using FME SDK example helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 10 @@ -10,4 +10,4 @@ sidebar_position: 10

-[.NET Core C# app project using Split SDK](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/net-core-CSharp-SDK) \ No newline at end of file +[.NET Core C# app project using FME SDK](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/net-core-CSharp-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/net-core-vb.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/net-core-vb.md index 4ef4f69e2f9..958a322d4f8 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/net-core-vb.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/net-core-vb.md @@ -1,6 +1,6 @@ --- -title: .NET Core VB using Split SDK example -sidebar_label: .NET Core VB using Split SDK example +title: .NET Core VB using FME SDK example +sidebar_label: .NET Core VB using FME SDK example helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 3 @@ -10,7 +10,7 @@ sidebar_position: 3

-Example: Basic code to use .NET Split SDK 6.0.1 +Example: Basic code to use FME .NET SDK 6.0.1 Environment: @@ -25,7 +25,7 @@ Environment: How to use: -* Update your relevant Split API key, user ID, and Split names in: +* Update your relevant FME API key, user ID, and feature flag names in: ``` Imports System @@ -61,7 +61,7 @@ Public Class Application properties("configType") = "INLINE" Common.Logging.LogManager.Adapter = New Common.Logging.NLog.NLogLoggerFactoryAdapter(properties) -' Using the Split SDK +' Using the FME SDK Dim splitConfig As ConfigurationOptions splitConfig = New ConfigurationOptions() Dim factory As SplitFactory @@ -72,7 +72,7 @@ Public Class Application System.Console.WriteLine("SDK is Ready") Dim treatment As String - treatment = client.GetTreatment("User ID","Split Name") + treatment = client.GetTreatment("User ID key","Feature flag name") System.Console.WriteLine(treatment) End Sub End Class diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/net-csharp.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/net-csharp.md index 1899f0fe3ce..b200c18022f 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/net-csharp.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/net-csharp.md @@ -1,6 +1,6 @@ --- -title: .NET C# app project using Split SDK example -sidebar_label: .NET C# app project using Split SDK example +title: .NET C# app project using FME SDK example +sidebar_label: .NET C# app project using FME SDK example helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 6 @@ -10,4 +10,4 @@ sidebar_position: 6

-[.NET C# App project using Split SDK](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/netCsharp-SDK) \ No newline at end of file +[.NET C# App project using FME SDK](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/netCsharp-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/php-app.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/php-app.md index a5e1ad3b76b..2c794467c08 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/php-app.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/php-app.md @@ -1,6 +1,6 @@ --- -title: PHP app project using Split SDK example -sidebar_label: PHP app project using Split SDK example +title: PHP app project using FME SDK example +sidebar_label: PHP app project using FME SDK example helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 8 @@ -10,4 +10,4 @@ sidebar_position: 8

-[PHP app project using Split SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/PHP-SDK) \ No newline at end of file +[PHP app project using FME SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/PHP-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/python-app.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/python-app.md index 5f58b503c3c..dd09b4942d3 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/python-app.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/python-app.md @@ -1,6 +1,6 @@ --- -title: Python app project using Split SDK example -sidebar_label: Python app project using Split SDK example +title: Python app project using FME SDK example +sidebar_label: Python app project using FME SDK example helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 11 @@ -10,4 +10,4 @@ sidebar_position: 11

-[Python app project using Split SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/Python-SDK) \ No newline at end of file +[Python app project using FME SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/Python-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/ruby-app.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/ruby-app.md index e6b06a567fc..6fcb606a7e0 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/ruby-app.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/ruby-app.md @@ -1,6 +1,6 @@ --- -title: Ruby app project using Split SDK example -sidebar_label: Ruby app project using Split SDK example +title: Ruby app project using FME SDK example +sidebar_label: Ruby app project using FME SDK example helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 9 @@ -10,4 +10,4 @@ sidebar_position: 9

-[Ruby app project using Split SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/Ruby-SDK) \ No newline at end of file +[Ruby app project using FME SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/Ruby-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/ruby-on-rails-puma.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/ruby-on-rails-puma.md index aae2476929e..eeb6a442880 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/ruby-on-rails-puma.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/ruby-on-rails-puma.md @@ -1,6 +1,6 @@ --- -title: Ruby on Rails with Puma App Engine project using Split SDK example -sidebar_label: Ruby on Rails with Puma App engine project using Split SDK example +title: Ruby on Rails with Puma App Engine project using FME SDK example +sidebar_label: Ruby on Rails with Puma App engine project using FME SDK example helpdocs_is_private: false helpdocs_is_published: true sidebar_position: 13 @@ -10,4 +10,4 @@ sidebar_position: 13

-[Ruby on Rails with Puma App Engine project using Split SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/Ruby-on-rail-Puma-SDK) \ No newline at end of file +[Ruby on Rails with Puma App Engine project using FME SDK example](https://github.com/Split-Community/Split-SDKs-Examples/tree/main/Ruby-on-rail-Puma-SDK) \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/ruby-sdk-rails-caching.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/ruby-sdk-rails-caching.md index 67f1c4ad858..4c73a1822b0 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/ruby-sdk-rails-caching.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdk-examples/ruby-sdk-rails-caching.md @@ -11,12 +11,12 @@ sidebar_position: 14

## Question -How can the Split SDK integrate with a Rails application that works with full page caching? +How can the Ruby SDK integrate with a Rails application that works with full page caching? ### Environment -We created a demo app to test Rails caching working with Split SDK. Rails Version: 5.0.7, Puma Version: 3.12.0 (standalone). Ruby Version: 2.2.2-p95 +We created a demo app to test Rails caching working with Ruby SDK. Rails Version: 5.0.7, Puma Version: 3.12.0 (standalone). Ruby Version: 2.2.2-p95 -We initialize the Split SDK as described in the [Split Documentation](https://docs.split.io/docs/ruby-sdk-overview#section-configuration) +We initialize the SDK as described in the [Split Documentation](https://docs.split.io/docs/ruby-sdk-overview#section-configuration) Initialization snippet (typically: config/initializers/split_client.rb) diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/elixir-thin-client-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/elixir-thin-client-sdk.md index ae98f2f53e7..f6133b9d004 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/elixir-thin-client-sdk.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/elixir-thin-client-sdk.md @@ -20,7 +20,7 @@ The Elixir Thin SDK supports Elixir language version v1.14.0 and later. ## Architecture -The Elixir Thin SDK depends on the [Split Daemon (splitd)](https://help.split.io/hc/en-us/articles/18305269686157) which should be set up on the same host. The Elixir Thin SDK client uses splitd to maintain the local cached copy of the Split rollout plan and return feature flag evaluations. +The Elixir Thin SDK depends on the [Split Daemon (splitd)](https://help.split.io/hc/en-us/articles/18305269686157) which should be set up on the same host. The Elixir Thin SDK factory client uses splitd to maintain the local cached copy of the FME definitions and return feature flag evaluations. ## Initialization @@ -103,7 +103,7 @@ end After you start the SDK, you can use the `Split.get_treatment/3` function to decide what version of your features your customers are served. The function requires the `FEATURE_FLAG_NAME` argument that you want to ask for a treatment and a unique `key` argument that corresponds to the end user that you want to serve the feature to. -From there, you simply need to use an if-else-if or case statement block as shown below and insert the code for the different treatments that you defined in the Split user interface. Remember the final else branch in your code to handle the client returning [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). +From there, you simply need to use an if-else-if or case statement block as shown below and insert the code for the different treatments that you defined in Harness FME. Remember the final else branch in your code to handle the client returning [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). ```elixir title="Elixir" ## The key here represents the string ID of the user/account/etc you're trying to evaluate a treatment for @@ -122,7 +122,7 @@ end To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK's `get_treatment` function needs to pass an attribute map at runtime. -In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `get_treatment` call in a map. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in the Split Web Console to decide whether to show the `on` or `off` treatment to this account. +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `get_treatment` call in a map. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in Harness FME to decide whether to show the `on` or `off` treatment to this account. The `get_treatment` function supports five types of attributes: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: @@ -146,7 +146,7 @@ treatment = Split.get_treatment("key", "FEATURE_FLAG_NAME", attributes); ### Multiple evaluations at once -In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `get_treatments` from the Split client to do this. +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `get_treatments` from the SDK factory client to do this. * `get_treatments`: Pass a list of the feature flag names you want treatments for. * `get_treatments_by_flag_set`: Evaluate all flags that are part of the provided set name and are cached on the SDK instance. * `get_treatments_by_flag_sets`: Evaluate all flags that are part of the provided set names and are cached on the SDK instance. @@ -181,7 +181,7 @@ You can also use the [Split Manager](#manager) to get all of your treatments at To [leverage dynamic configurations with your treatments](https://help.split.io/hc/en-us/articles/360026943552), you should use the `Split.get_treatment_with_config/3` function. This function returns an `Split.TreatmentWithConfig` struct containing the treatment and associated configuration. -The config element is a stringified version of the configuration JSON defined in the Split user interface. If there is no configuration defined for a treatment, the SDK returns `nil` for the config parameter. +The config element is a stringified version of the configuration JSON defined in Harness FME. If there is no configuration defined for a treatment, the SDK returns `nil` for the config parameter. This function takes the exact same set of arguments as the standard `Split.get_treatment/3` function. See below for examples on proper usage: @@ -210,23 +210,23 @@ Due to the nature of the Elixir SDK, which uses the Split Daemon, there is no ne ## Track -Use the `Split.track/5` function to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users’ actions and metrics. +Use the `Split.track/5` function to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to and allows you to measure the impact of your feature flags on your users’ actions and metrics. Refer to the [Events](https://help.split.io/hc/en-us/articles/360020585772) documentation for more information about using track events in feature flags. In the examples below you can see that the `Split.track/5` function can take up to five arguments. The proper data type and syntax for each are: * **key:** The `key` variable used in the `get_treatment` call and firing this track event. The expected data type is **String**. -* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in Harness FME. * **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: * Contains 63 characters or fewer. * Starts with a letter or number. * Contains only letters, numbers, hyphen, underscore, or period. * This is the regular expression we use to validate the value:
`[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` * **VALUE:** (Optional) The value to be used in creating the metric. This field can be sent in as `nil` or `0` if you intend to purely use the count function when creating a metric. The expected data type is **Integer** or **Float**. -* **PROPERTIES:** (Optional) A Map of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. +* **PROPERTIES:** (Optional) A Map of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. FME currently supports three types of properties: strings, numbers, and booleans. -The `track` function returns a boolean value of `true` or `false` to indicate whether or not the event was successfully queued to be sent back to Split's servers on the next event post. The SDK will return `false` if it wasn't able to connect to the Split Daemon, or if the current queue on the Split Daemon is full, or if an incorrect input to the `track` function has been provided. +The `track` function returns a boolean value of `true` or `false` to indicate whether or not the event was successfully queued to be sent back to Harness servers on the next event post. The SDK will return `false` if it wasn't able to connect to the Split Daemon, or if the current queue on the Split Daemon is full, or if an incorrect input to the `track` function has been provided. In the case that a bad input has been provided, you can read more about our SDK's expected behavior in the [Events documentation](https://help.split.io/hc/en-us/articles/360020585772-Track-events) diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/go-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/go-sdk.md index faddfa04dd1..5c192a2f01d 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/go-sdk.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/go-sdk.md @@ -24,9 +24,9 @@ The Go SDK supports Go language version 1.18 and above. The Go SDK can run in three different modes to fit in different infrastructure configurations. -* **in-memory-standalone:** The default (if no mode is specified) and most straightforward operation mode uses an in-memory storage to keep feature flags, segments, and queued impressions/metrics, as well as its own synchronization tasks that periodically keep feature flags and segments up to date, while flushing impressions and metrics to the Split backend. -* **redis-consumer:** This mode uses Redis as a broker to retrieve feature flags and segments and store impressions and metrics. It also requires the [Split Synchronizer](https://help.split.io/hc/en-us/articles/360019686092) to be running in the background, populating Redis with segments and feature flags, and flushing impressions and metrics to the Split backend. This mode is useful if you have multiple instances of Split's SDKs running (either in the same or a different language) and want to have a single synchronization point in your infrastructure. -* **localhost:** This mode should be used to stub the Split service when running local tests or development processes. It parses a file (either one specified by the user or `$HOME/.splits`) that defines feature flags and treatments to provide the developer with a predictable result of running `Treatment()` calls. +* **in-memory-standalone:** The default (if no mode is specified) and most straightforward operation mode uses an in-memory storage to keep feature flags, segments, and queued impressions/metrics, as well as its own synchronization tasks that periodically keep feature flags and segments up to date, while flushing impressions and metrics to the Harness servers. +* **redis-consumer:** This mode uses Redis as a broker to retrieve feature flags and segments and store impressions and metrics. It also requires the [Split Synchronizer](https://help.split.io/hc/en-us/articles/360019686092) to be running in the background, populating Redis with segments and feature flags, and flushing impressions and metrics to the Harness servers. This mode is useful if you have multiple instances of SDKs running (either in the same or a different language) and want to have a single synchronization point in your infrastructure. +* **localhost:** This mode should be used to stub the FME service when running local tests or development processes. It parses a file (either one specified by the user or `$HOME/.splits`) that defines feature flags and treatments to provide the developer with a predictable result of running `Treatment()` calls. ### 1. Installing the SDK into your Go environment @@ -47,7 +47,7 @@ go get github.com/splitio/go-client/v6@v6.7.0 ``` :::warning[If using Synchronizer with Redis - Synchronizer 2.x required after SDK Version 5.0.0] -Since version 2.0.0 of the split-synchronizer, we use a more efficient scheme to store impressions in Redis. This approach is faster and easier on your Redis instances, since it yields better throughput of impressions to the backend. If you use this SDK with the synchronizer in Redis or Proxy mode, you will need the newest versions of our Split synchronizer. It is recommended that once you're using SDK versions compatible with Split-Sync 2.0 on all your applications pointing to the Redis instance maintained by the Split-Sync, you disable backwards compatibility (this is as easy as changing a parameter to `true` on the JSON config or an environment variable to `on` if you're using the docker image). +Since version 2.0.0 of the split-synchronizer, we use a more efficient scheme to store impressions in Redis. This approach is faster and easier on your Redis instances, since it yields better throughput of impressions to the backend. If you use this SDK with the synchronizer in Redis or Proxy mode, you will need the newest versions of our Split Synchronizer. It is recommended that once you're using SDK versions compatible with Split-Sync 2.0 on all your applications pointing to the Redis instance maintained by the Split-Sync, you disable backwards compatibility (this is as easy as changing a parameter to `true` on the JSON config or an environment variable to `on` if you're using the docker image). ::: ### 2. Import the SDK into your project @@ -66,7 +66,7 @@ Starting on version v6.0.0, every breaking change will require that you update y It is recommended to create a wrapper that keeps it encapsulated. The package/file should be responsible for instantiating a single instance and exposing its functionality. ::: -### 3. Instantiate the SDK and create a new Split client +### 3. Instantiate the SDK and create a new SDK factory client :::danger[If upgrading an existing SDK - Block until ready changes] Starting version 4.0.0, cfg.BlockUntilReady is deprecated and migrated to the following implementation: @@ -77,13 +77,13 @@ When the SDK is instantiated in `inmemory-standalone` operation mode, it kicks o This process can take up to a few hundred milliseconds depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while it is in this intermediate state, it may not have the data necessary to run the evaluation. In this circumstance, the SDK does not fail, but instead returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). -To make sure the SDK is properly loaded before asking it for a treatment, you need to block until the SDK is ready. You can block by using the `BlockUntilReady(int)` method as part of the instantiation process of the SDK client as shown below. Do this as a part of the startup sequence of your application. +To make sure the SDK is properly loaded before asking it for a treatment, you need to block until the SDK is ready. You can block by using the `BlockUntilReady(int)` method as part of the instantiation process of the SDK factory client as shown below. Do this as a part of the startup sequence of your application. Instantiating two (or more) different factories results in multiple instances of synchronization tasks, so you can have different instances of the SDK with different SDK Keys running within a single application. -In the most common scenario, you should instantiate and reuse a single Split factory throughout your application. +In the most common scenario, you should instantiate and reuse a single SDK factory throughout your application. -Configure the SDK with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split user interface, on your Admin settings page, API keys section. Select a server-side SDK API key. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. +Configure the SDK with the SDK key for the FME environment that you would like to access. The SDK key is available in Harness FME Admin settings. Select a server-side SDK API key. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. ```go title="Go" func main() { @@ -110,9 +110,9 @@ Now you can start asking the SDK to evaluate treatments for your customers. ### Basic use -After you instantiate the SDK client, you can start using the client's `Treatment` method to decide what version of your feature flags your customers are served. The method requires the `FEATURE_FLAG_NAME` attribute that you want to ask for a treatment and a unique `key` attribute that corresponds to the end user that you are serving the feature flag to. +After you instantiate the SDK factory client, you can start using the client's `Treatment` method to decide what version of your feature flags your customers are served. The method requires the `FEATURE_FLAG_NAME` attribute that you want to ask for a treatment and a unique `key` attribute that corresponds to the end user that you are serving the feature flag to. -Then use an if-else-if block as shown below and insert the code for the different treatments that you defined in the Split user interface. Remember the final else branch in your code to handle the client returning the [control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). +Then use an if-else-if block as shown below and insert the code for the different treatments that you defined in Harness FME. Remember the final else branch in your code to handle the client returning the [control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). ```go title="Go" // The key here represents the ID of the user/account/etc you're trying to evaluate a treatment for @@ -136,7 +136,7 @@ The arguments for the `Treatment()` call are: To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK's `Treatment` method needs to be passed an attribute map at runtime. -In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `Treatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in the Split user interface to decide whether to show the `on` or off` treatment to this account. +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `Treatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in Harness FME to decide whether to show the `on` or off` treatment to this account. The `Treatment()` method supports five types of attributes: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: @@ -157,7 +157,7 @@ The `Treatment()` method supports five types of attributes: strings, numbers, da ### Multiple evaluations at once -In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `Treatments` from the Split client to do this. +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `Treatments` from the SDK factory client to do this. * `Treatments`: Pass a list of the feature flag names you want treatments for. * `TreatmentsByFlagSet`: Evaluate all flags that are part of the provided set name and are cached on the SDK instance. * `TreatmentsByFlagSets`: Evaluates all flags that are part of the provided set names and are cached on the SDK instance. @@ -198,7 +198,7 @@ To [leverage dynamic configurations with your treatments](https://help.split.io/ This method will return an object containing the treatment and associated configuration. -The config element will be a stringified version of the configuration JSON defined in the Split user interface. If there are no configs defined for a treatment, the SDK returns `nil` for the config parameter. +The config element will be a stringified version of the configuration JSON defined in Harness FME. If there are no configs defined for a treatment, the SDK returns `nil` for the config parameter. This method takes the exact same set of arguments as the standard `Treatment` method. See below for examples on proper usage: @@ -243,7 +243,7 @@ TreatmentResults := splitClient.TreatmentsWithConfigByFlagSets("KEY", []string{" ### Shutdown -Call the `.Destroy()` method before letting a process using the SDK exit, as this method gracefully shuts down the Split SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. Call the `splitClient.Destroy()` method when the `kill` signal is cached by your application. After `.Destroy()` is called, any subsequent invocations to the `splitClient.Treatment()` or `manager` methods results in `control` or empty list, respectively. +Call the `.Destroy()` method before letting a process using the SDK exit, as this method gracefully shuts down the SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. Call the `splitClient.Destroy()` method when the `kill` signal is cached by your application. After `.Destroy()` is called, any subsequent invocations to the `splitClient.Treatment()` or `manager` methods results in `control` or empty list, respectively. The example below shows how to catch the stop signal and call the `Destroy()` method. @@ -283,24 +283,24 @@ A call to the `destroy()` method also destroys the factory object. When creating ## Track -Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users’ actions and metrics. +Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to and allows you to measure the impact of your feature flags on your users’ actions and metrics. [Learn more](https://help.split.io/hc/en-us/articles/360020585772) about using track events in feature flags. In the examples below, you can see that the `Track()` method can take up to five arguments. The proper data type and syntax for each are: * **key:** The `key` variable used in the `Treatment` call and firing this track event. The expected data type is **String**. -* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in Harness FME. * **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: * Contains 63 characters or fewer. * Starts with a letter or number. * Contains only letters, numbers, hyphen, underscore, or period. * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` * **VALUE:** (Optional) The value to be used in creating the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is **Integer** or **Float**. -* **PROPERTIES:** (Optional) A Map of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. +* **PROPERTIES:** (Optional) A Map of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. FME currently supports three types of properties: strings, numbers, and booleans. :::warning[Redis Support] -If you are using our SDK with Redis, you need Split synchronizer **2.3.0** version at least in order to support *properties* in the `track` method. +If you are using our SDK with Redis, you need Split Synchronizer **2.3.0** version at least in order to support *properties* in the `track` method. ::: ```go title="Go" @@ -345,8 +345,8 @@ The SDK has a number of knobs for configuring performance. Each knob is tuned to | Redis | Describes the Redis connection information (host, port, etc.) and allows you to specify a prefix to avoid conflicts with other SDKs. | See Redis section. | | SplitFile | Filename to be used when operating in `localhost` mode. | `.splits` within the user's home folder | | TaskPeriods | Embedded struct that allows the developer to choose how frequently each synchronization task is run. | See TaskPeriods section. | -| IPAddressesEnabled | Flag to disable IP addresses and host name from being sent to the Split backend. | true | -| ImpressionsMode | Defines how impressions are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Split; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, all impressions are queued and sent to Split; this is useful for validations. Use DEBUG mode when you want every impression to be logged in Split's user interface when trying to debug your SDK setup. This setting does not impact the impression listener which receives all generated impressions locally. | `optimized` | +| IPAddressesEnabled | Flag to disable IP addresses and host name from being sent to the Harness servers. | true | +| ImpressionsMode | Defines how impressions are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Harness; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Harness FME and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, all impressions are queued and sent to Harness; this is useful for validations. Use DEBUG mode when you want every impression to be logged in Harness FME when trying to debug your SDK setup. This setting does not impact the impression listener which receives all generated impressions locally. | `optimized` | The SDK factory receives two arguments, the API key and a pointer to a configuration structure. @@ -464,11 +464,11 @@ This configuration structure can be used to change the execution period of each ## Localhost mode -For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the Split SDK can be started in localhost mode (also known as off-the-grid mode). In this mode, the SDK neither polls nor updates Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the feature flags. To use the SDK in localhost mode, you must replace the API key with localhost value. +For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the SDK can be started in **localhost** mode (also known as off-the-grid mode). In this mode, the SDK neither polls nor updates Harness servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the feature flags. To use the SDK in localhost mode, you must replace the API key with localhost value. With this mode, you can instantiate the SDKS using one of the following methods: -* JSON: Full support, for advanced cases or replicating an environment by pulling rules from Split cloud (from version `v6.3.0`). +* JSON: Full support, for advanced cases or replicating an environment by pulling rules from Harness FME servers (from version `v6.3.0`). * YAML: Supports dynamic configs, individual targets, and default rules (from version `4.0.0`). * .split: Legacy option, only treatment result. @@ -714,7 +714,7 @@ new-navigation v3 ## Manager -Use the Split Manager to get a list of feature flags available to the Split client. +Use the Split Manager to get a list of feature flags available to the SDK factory client. To instantiate a Manager in your code base, use the same factory that you used for your client. @@ -768,7 +768,7 @@ type SplitView struct { ## Listener -Split SDKs send impression data back to Split servers periodically when evaluating feature flags. To send this information to a location of your choice, define and attach an *impression listener*. Use the SDK's `impressionListener` parameter, where you can add an implementation of `ImpressionListener`. This implementation **must** define the `LogImpression` method. It receives data in the following schema. +FME SDKs send impression data back to Harness servers periodically when evaluating feature flags. To send this information to a location of your choice, define and attach an *impression listener*. Use the SDK's `impressionListener` parameter, where you can add an implementation of `ImpressionListener`. This implementation **must** define the `LogImpression` method. It receives data in the following schema. | **Name** | **Type** | **Description** | | --- | --- | --- | diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/java-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/java-sdk.md index cf8cf12d46a..0eeb64a1600 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/java-sdk.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/java-sdk.md @@ -20,7 +20,7 @@ The Java SDK supports JDK8 and later. ## Initialization -To get started, set up Split in your code base using the following two steps. +To get started, set up FME in your code base using the following two steps. ### 1. Import the SDK into your project @@ -58,7 +58,7 @@ If you cannot find the dependency, it may be due to the lag in the sync time bet
-### 2. Instantiate the SDK and create a new Split client +### 2. Instantiate the SDK and create a new SDK factory client :::danger[If upgrading an existing SDK - Block until ready changes] Starting version 3.0.1, SplitClientConfig#ready(int) is deprecated and migrated to a two part implementation: @@ -66,13 +66,13 @@ Starting version 3.0.1, SplitClientConfig#ready(int) is deprecated and migrated * Call `SplitClient#blockUntilReady()` or `SplitManager#blockUntilReady()`. ::: -When the SDK is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds, depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while it's in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). +When the SDK is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Harness servers. This process can take up to a few hundred milliseconds, depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while it's in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). To make sure the SDK is properly loaded before asking it for a treatment, block until the SDK is ready. Do this by setting the desired wait using `.setBlockUntilReadyTimeout()` in the configuration and calling `blockUntilReady()` on the client. Do this all as a part of the startup sequence of your application. -We recommend instantiating the Split factory once as a singleton and reusing it throughout your application. +We recommend instantiating the SDK factory once as a singleton and reusing it throughout your application. -Use the code snippet below with your own API key. Configure the SDK with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split user interface, on your Admin settings page, API keys section. Select a server-side SDK API key. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. +Use the code snippet below with your own API key. Configure the SDK with the SDK key for the FME environment that you would like to access. The SDK key is available in Harness FME Admin settings. Select a server-side SDK API key. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. @@ -122,9 +122,9 @@ Now you can start asking the SDK to evaluate treatments for your customers. ### Basic use -After you instantiate the SDK client, you can start using the `getTreatment` method of the SDK client to decide what version of your features your customers are served. The method requires the `FEATURE_FLAG_NAME` attribute that you want to ask for a treatment and a unique `key` attribute that corresponds to the end user that you are serving the feature to. +After you instantiate the SDK factory client, you can start using the `getTreatment` method of the SDK factory client to decide what version of your features your customers are served. The method requires the `FEATURE_FLAG_NAME` attribute that you want to ask for a treatment and a unique `key` attribute that corresponds to the end user that you are serving the feature to. -Then use an if-else-if block as shown below and insert the code for the different treatments that you defined in the Split user interface. Remember the final else branch in your code to handle the client returning the [control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). +Then use an if-else-if block as shown below and insert the code for the different treatments that you defined in Harness FME. Remember the final else branch in your code to handle the client returning the [control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). @@ -165,7 +165,7 @@ when (treatment) { To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK's `getTreatment` method needs to be passed an attribute map at runtime. -In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the Rollout plan as defined in the Split user interface to decide whether to show the `on` or `off` treatment to this account. +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the Rollout plan as defined in Harness FME to decide whether to show the `on` or `off` treatment to this account. The `getTreatment` method supports five types of attributes: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: @@ -243,7 +243,7 @@ when (treatment) { ### Multiple evaluations at once -In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` from the Split client to do this. +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` from the SDK factory client to do this. * `getTreatments`: Pass a list of the feature flag names you want treatments for. * `getTreatmentsByFlagSet`: Evaluate all flags that are part of the provided set name and are cached on the SDK instance. * `getTreatmentsByFlagSets`: Evaluate all flags that are part of the provided set names and are cached on the SDK instance. @@ -283,7 +283,7 @@ val treatmentsBySets = client.getTreatmentsByFlagSets("KEY", flagSetNames) To [leverage dynamic configurations with your treatments](https://help.split.io/hc/en-us/articles/360026943552), you should use the `getTreatmentWithConfig` method. This method returns an object containing the treatment and associated configuration. -The config element is a stringified version of the configuration JSON defined in the Split user interface. If there is no configuration defined for a treatment, the SDK returns `null` for the config parameter. +The config element is a stringified version of the configuration JSON defined in Harness FME. If there is no configuration defined for a treatment, the SDK returns `null` for the config parameter. This method takes the exact same set of arguments as the standard `getTreatment` method. See below for examples on proper usage: @@ -341,7 +341,7 @@ val treatmentsBySets: Map = client.getTreatmentsWithConfigB ### Shutdown -Make sure to call `.destroy()` before letting a process using the SDK exit as it gracefully shuts down the Split SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions and events. The Java SDK specifically subscribes to the JVM shutdown hook (SIGTERM signal) which in normal circumstances is invoked automatically by the JVM during a shutdown process. This means that on a graceful shutdown of the server, the client will automatically call destroy() and will flush the buffers and release the resources. +Make sure to call `.destroy()` before letting a process using the SDK exit as it gracefully shuts down the SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions and events. The Java SDK specifically subscribes to the JVM shutdown hook (SIGTERM signal) which in normal circumstances is invoked automatically by the JVM during a shutdown process. This means that on a graceful shutdown of the server, the client will automatically call destroy() and will flush the buffers and release the resources. In cases where you don't want our SDK to automatically destroy on shutdown, you can use the config: `disableDestroyOnShutDown()` (example usage in the [Configuration](#configuration) section below) and set it to `true`. If you do this, the SDK ignores any signals like SIGTERM and it is your responsibility to properly call destroy at the right time. If a manual shutdown is required, you can then call: @@ -366,23 +366,23 @@ A call to the `destroy()` method also destroys the factory object. When creating ## Track -Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users' actions and metrics. +Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to and allows you to measure the impact of your feature flags on your users' actions and metrics. Refer to the [Events](https://help.split.io/hc/en-us/articles/360020585772) guide for more information about using track events in feature flags. In the examples below you can see that the `.track()` method can take up to five arguments. The proper data type and syntax for each are: * **key:** The `key` variable used in the `getTreatment` call and firing this track event. The expected data type is **String**. -* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you defined in your instance of Split. +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you defined in Harness FME. * **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: * Contains 63 characters or fewer. * Starts with a letter or number. * Contains only letters, numbers, hyphen, underscore, or period. * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` * **VALUE:** (Optional) The value used in creating the metric. This field can be sent in as null or 0 if you intend to only use the count function when creating a metric. The expected data type is **Integer** or **Float**. -* **PROPERTIES:** (Optional) A map of key value pairs that can filter your metrics. To learn more about event property capture, refer to the [Events property capture](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. +* **PROPERTIES:** (Optional) A map of key value pairs that can filter your metrics. To learn more about event property capture, refer to the [Events property capture](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. FME currently supports three types of properties: strings, numbers, and booleans. -The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK successfully queued the event to be sent back to Split's servers on the next event post. The SDK returns `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method is provided. +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK successfully queued the event to be sent back to Harness servers on the next event post. The SDK returns `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method is provided. In the case that a bad input has been provided, you can read more about our SDK's expected behavior in our [Events](https://help.split.io/hc/en-us/articles/360020585772-Track-events) guide. @@ -459,24 +459,24 @@ The SDK has a number of knobs for configuring performance. Each knob is tuned to | **Configuration** | **Description** | **Default value** | | --- | --- | --- | -| featuresRefreshRate | The SDK polls Split servers for changes to feature rollout plans. This parameter controls this polling period in seconds. | 60 seconds | -| segmentsRefreshRate | The SDK polls Split servers for changes to segments at this rate (in seconds). | 60 seconds | -| impressionsRefreshRate | The treatment log captures which customer saw what treatment (on, off, etc.) at what time. This log is periodically flushed back to Split servers. This configuration controls how quickly the cache expires after a write (in seconds). | 60 seconds | -| telemetryRefreshRate | The SDK caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers (in seconds). | 3600 seconds | +| featuresRefreshRate | The SDK polls Harness servers for changes to feature rollout plans. This parameter controls this polling period in seconds. | 60 seconds | +| segmentsRefreshRate | The SDK polls Harness servers for changes to segments at this rate (in seconds). | 60 seconds | +| impressionsRefreshRate | The treatment log captures which customer saw what treatment (on, off, etc.) at what time. This log is periodically flushed back to Harness servers. This configuration controls how quickly the cache expires after a write (in seconds). | 60 seconds | +| telemetryRefreshRate | The SDK caches diagnostic data that it periodically sends to Harness servers. This configuration controls how frequently this data is sent back to Harness servers (in seconds). | 3600 seconds | | eventsQueueSize | When using `.track`, the number of events to be kept in memory. | 500 | -| eventFlushIntervalInMillis | When using `.track`, how often (in milliseconds) the events queue is flushed to Split servers. | 30000 ms | +| eventFlushIntervalInMillis | When using `.track`, how often (in milliseconds) the events queue is flushed to Harness servers. | 30000 ms | | connectionTimeout | HTTP client connection timeout (in ms). | 15000ms | | readTimeout | HTTP socket read timeout (in ms). | 15000ms | | setBlockUntilReadyTimeout | If specified, the client building process blocks until the SDK is ready to serve traffic or the specified time has elapsed. If the SDK is not ready within the specified time, a `TimeOutException` is thrown (in ms). | 0ms | | impressionsQueueSize | Default queue size for impressions. | 30K | -| disableLabels | Disable labels from being sent to Split backend. Labels may contain sensitive information. | enabled | +| disableLabels | Disable labels from being sent to Harness servers. Labels may contain sensitive information. | enabled | | disableIPAddress | Disable sending IP Address & hostname to the backend. | enabled | | proxyHost | The location of the proxy. | localhost | | proxyPort | The port of the proxy. | -1 (not set) | | proxyUsername | Username to authenticate against the proxy server. | null | | proxyPassword | Password to authenticate against the proxy server. | null | | streamingEnabled | Boolean flag to enable the streaming service as default synchronization mechanism. In the event of an issue with streaming, the SDK falls back to the polling mechanism. If false, the SDK polls for changes as usual without attempting to use streaming. | true | -| impressionsMode | Defines how impressions are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Split; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, all impressions are queued and sent to Split; this is useful for validations. Use DEBUG mode when you want every impression to be logged in the Split user interface when trying to debug your SDK setup. This setting does not impact the impression listener which receives all generated impressions locally. | OPTIMIZED | +| impressionsMode | Defines how impressions are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Harness; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Harness FME and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, all impressions are queued and sent to Harness; this is useful for validations. Use DEBUG mode when you want every impression to be logged in Harness FME when trying to debug your SDK setup. This setting does not impact the impression listener which receives all generated impressions locally. | OPTIMIZED | | operationMode | Defines how the SDK synchronizes its data. Two operation modes are currently supported:
- STANDALONE.
- CONSUMER| STANDALONE | | storageMode | Defines what kind of storage the SDK is going to use. With MEMORY, the SDK uses its own storage and runs as STANDALONE mode. Set REDIS mode if you want the SDK to run with this implementation as CONSUMER mode. | MEMORY | | flagSetsFilter | This setting allows the SDK to only synchronize the feature flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the SDK instance, bringing all the benefits from a reduced payload. | null | @@ -533,9 +533,9 @@ client.blockUntilReady() ## Connecting to a Split Proxy instance -The SDK can connect to a Split Proxy instance as though it was connecting to our CDN, and the Proxy synchronizes the data and writes impressions and events back to the Split server. Be sure to install the Split Proxy by following the steps in [Split Proxy guide](https://help.split.io/hc/en-us/articles/4415960499213-Split-Proxy). +The SDK can connect to a Split Proxy instance as though it was connecting to our CDN, and the Proxy synchronizes the data and writes impressions and events back to Harness FME servers. Be sure to install the Split Proxy by following the steps in [Split Proxy guide](https://help.split.io/hc/en-us/articles/4415960499213-Split-Proxy). -Use the `.endpoint()` property in the SplitClientConfig builder object to point the Java SDK to the Synchronizer, making sure to use the same port specified in the Proxy command line. When creating the `SplitFactory` object, use the custom API key specified in the `client-apikeys` parameter for the Proxy. The Proxy uses the Split SDK key when connecting to Split. Refer to the following code example to connect to a Proxy instance: +Use the `.endpoint()` property in the SplitClientConfig builder object to point the Java SDK to the Synchronizer, making sure to use the same port specified in the Proxy command line. When creating the `SplitFactory` object, use the custom API key specified in the `client-apikeys` parameter for the Proxy. The Proxy uses the SDK key when connecting to Harness FME servers. Refer to the following code example to connect to a Proxy instance: @@ -612,11 +612,11 @@ fun main (args: Array){ ## Localhost mode -For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the Split SDK can be started in **localhost** mode (aka off-the-grid mode). In this mode, the SDK neither polls nor updates Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the features. To use the SDK in localhost mode, you must replace the API Key with "localhost" value. +For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the SDK can be started in **localhost** mode (aka off-the-grid mode). In this mode, the SDK neither polls nor updates Harness servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the features. To use the SDK in localhost mode, you must replace the API Key with "localhost" value. With this mode, you can instantiate the SDKS using one of the following methods: -* JSON: Full support, for advanced cases or replicating an environment by pulling rules from Split cloud (from version `4.7.0`). +* JSON: Full support, for advanced cases or replicating an environment by pulling rules from Harness FME servers (from version `4.7.0`). * YAML: Supports dynamic configs, individual targets and default rules (from version `3.1.0`). * .split: Legacy option, only treatment result. @@ -790,7 +790,7 @@ class Split( #### segmentDirectory -The provided segment directory must have the json files of the corresponding segment linked to previous feature flag definitions. According to the Split file sample above: `feature_flag_1` has `segment_1` linked. That means that the segmentDirectory needs to have `segment_1` definition. +The provided segment directory must have the json files of the corresponding segment linked to previous feature flag definitions. According to the file sample above: `feature_flag_1` has `segment_1` linked. That means that the segmentDirectory needs to have `segment_1` definition. @@ -889,7 +889,7 @@ In the example above, we have four entries: * The third entry defines that `my_feature_flag` always returns `off` for all keys that don't match another entry (in this case, any key other than `key`). * The fourth entry shows how an example overrides a treatment for a set of keys. -Use the SplitConfigBuilder object to set the location of the Split localhost YAML file as shown in the example below: +Use the SplitConfigBuilder object to set the location of the localhost YAML file as shown in the example below: @@ -1043,7 +1043,7 @@ compile 'io.split.client:redis-wrapper:1.0.0' Set up the [Split Synchronizer](https://help.split.io/hc/en-us/articles/360019686092) to sync data to a Redis cache. Once you set up the synchronizer, go to the following step #3 to instantiate: -#### 3. Instantiate the SDK client with Redis enabled +#### 3. Instantiate the SDK factory client with Redis enabled To run the SDK with Redis, you need to provide the Redis storage wrapper. Refer to the following to provide the wrapper: @@ -1171,7 +1171,7 @@ The Java SDK performs multi-key operations in certain methods such as `mget` (to ## Manager -Use the Split Manager to get a list of features available to the Split client. +Use the Split Manager to get a list of features available to the SDK factory client. To instantiate a Manager in your code base, use the same factory that you used for your client. @@ -1276,7 +1276,7 @@ class SplitView( ## Listener -Split SDKs send impression data back to Split servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression listener*. +FME SDKs send impression data back to Harness servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an *impression listener*. The SDK sends the generated impressions to the impression listener immediately. As a result, be careful while implementing handling logic to avoid blocking the main thread. As the second parameter, specify the size of the queue acting as a buffer (see the snippet below). @@ -1470,7 +1470,7 @@ client.blockUntilReady() ### New Relic -The New Relic integration annotates New Relic transactions with Split feature flags information that can be used to correlate application metrics with feature flag changes. This integration is implemented as a synchronous impression listener and it can be enabled as shown below: +The New Relic integration annotates New Relic transactions with FME feature flags information that can be used to correlate application metrics with feature flag changes. This integration is implemented as a synchronous impression listener and it can be enabled as shown below: @@ -1499,7 +1499,7 @@ SplitFactoryBuilder.build("YOUR_SDK_KEY", config).client() -This integration is only enabled if Split SDK detects the New Relic agent in the classpath. If the agent is not detected, the following error will be displayed in the logs (if logging is enabled): +This integration is only enabled if the SDK detects the New Relic agent in the classpath. If the agent is not detected, the following error will be displayed in the logs (if logging is enabled): ``` WARN [main] (IntegrationsConfig.java:72) - New Relic agent not found. Continuing without it ``` @@ -1510,7 +1510,7 @@ If you need to use a network proxy, you can configure proxies by setting the `pr ## Advanced: WebLogic container -WebLogic and the Split Java SDK contain a reference to Google Guava. If you are currently deploying a web application that contains our Java SDK into WebLogic, instruct the container to load Guava from the app classpath and not from the container. +WebLogic and the Java SDK contain a reference to Google Guava. If you are currently deploying a web application that contains our Java SDK into WebLogic, instruct the container to load Guava from the app classpath and not from the container. If you have an existing **weblogic.xml** file in your deployment, add: `com.google.common.*` under the `` tag. If you do not, create the file and place it under the directory `WEB-INF`. diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/net-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/net-sdk.md index acc78e34f22..6f932abad4f 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/net-sdk.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/net-sdk.md @@ -35,20 +35,20 @@ Use NuGet in the command line or the Package Manager UI in Visual Studio. Install-Package Splitio -Version 7.10.0 ``` -### 2. Instantiate the SDK and create a new Split client +### 2. Instantiate the SDK and create a new SDK factory client :::danger[If upgrading an existing SDK - Block until ready changes] Starting version 5.0.0, .Ready is deprecated and migrated to the following implementation: Call `SplitClient.BlockUntilReady(int milliseconds)` or `SplitManager.BlockUntilReady(int milliseconds)`. ::: -When the SDK is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while its in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). +When the SDK is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Harness servers. This process can take up to a few hundred milliseconds depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while its in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). -To make sure the SDK is properly loaded before asking it for a treatment, block until the SDK is ready. This is done by using `.BlockUntilReady(int milliseconds)` method as part of the instantiation process of the SDK client as shown below. Do this all as a part of the startup sequence of your application. If SDK is not ready after the specified time, the SDK fails to initialize and throws a `TimeoutException` error. +To make sure the SDK is properly loaded before asking it for a treatment, block until the SDK is ready. This is done by using `.BlockUntilReady(int milliseconds)` method as part of the instantiation process of the SDK factory client as shown below. Do this all as a part of the startup sequence of your application. If SDK is not ready after the specified time, the SDK fails to initialize and throws a `TimeoutException` error. -We recommend instantiating the Split factory once as a singleton and reusing it throughout your application. +We recommend instantiating the SDK factory once as a singleton and reusing it throughout your application. -Use the code snippet below with your own API key. Configure the SDK with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split user interface, on your Admin settings page, API keys section. Select a server-side SDK API key. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. +Use the code snippet below with your own API key. Configure the SDK with the SDK key for the FME environment that you would like to access. The SDK key is available in Harness FME Admin settings. Select a server-side SDK API key. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. ```csharp title="C#" using Splitio.Services.Client.Classes; @@ -74,9 +74,9 @@ Now you can start asking the SDK to evaluate treatments for your customers. ### Basic use -After you instantiate the SDK client, you can use the `GetTreatment` method of the SDK client to decide what version of your features your customers are served. The method requires the `FEATURE_FLAG_NAME` attribute that you want to ask for a treatment and a unique `key` attribute that corresponds to the end user that you are serving the feature to. +After you instantiate the SDK factory client, you can use the `GetTreatment` method of the SDK factory client to decide what version of your features your customers are served. The method requires the `FEATURE_FLAG_NAME` attribute that you want to ask for a treatment and a unique `key` attribute that corresponds to the end user that you are serving the feature to. -Then use an if-else-if block as shown below and insert the code for the different treatments that you defined in the Split user interface. Remember the final else branch in your code to handle the client returning the [control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). +Then use an if-else-if block as shown below and insert the code for the different treatments that you defined in Harness FME. Remember the final else branch in your code to handle the client returning the [control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). @@ -123,7 +123,7 @@ else To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK's `GetTreatment` method needs to be passed an attribute map at runtime. -In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `GetTreatment` call. These attributes are compared and evaluated against the attributes used in the Rollout plan as defined in the Split user interface to decide whether to show the `on` or `off` treatment to this account. +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `GetTreatment` call. These attributes are compared and evaluated against the attributes used in the Rollout plan as defined in Harness FME to decide whether to show the `on` or `off` treatment to this account. The `GetTreatment` method supports five types of attributes: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: @@ -224,7 +224,7 @@ else ### Multiple evaluations at once -In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `GetTreatments` from the Split client to do this. +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `GetTreatments` from the SDK factory client to do this. * `GetTreatments`: Pass a list of the feature flag names you want treatments for. * `GetTreatmentsByFlagSet`: Evaluate all flags that are part of the provided set name and are cached on the SDK instance. * `GetTreatmentsByFlagSets`: Evaluate all flags that are part of the provided set names and are cached on the SDK instance. @@ -274,7 +274,7 @@ To [leverage dynamic configurations with your treatments](https://help.split.io/ This method returns an object containing the treatment and associated configuration. -The config element is a stringified version of the configuration JSON defined in the Split user interface. If there is no configuration defined for a treatment, the SDK returns `null` for the config parameter. +The config element is a stringified version of the configuration JSON defined in Harness FME. If there is no configuration defined for a treatment, the SDK returns `null` for the config parameter. This method takes the exact same set of arguments as the standard `GetTreatment` method. See below for examples on proper usage: @@ -336,7 +336,7 @@ var featureFlagResults = await splitClient.GetTreatmentsWithConfigByFlagSetsAsyn ### Shutdown -Call the `.destroy()` method before letting a process using the SDK exit, as this method gracefully shuts down the Split SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. +Call the `.destroy()` method before letting a process using the SDK exit, as this method gracefully shuts down the SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. If a manual shutdown is required, call the `client.Destroy()` method. @@ -359,23 +359,23 @@ A call to the `destroy()` method also destroys the factory object. When creating ## Track -Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users’ actions and metrics. +Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to and allows you to measure the impact of your feature flags on your users’ actions and metrics. [Learn more](https://help.split.io/hc/en-us/articles/360020585772) about using track events in feature flags. In the examples below, you can see that the `.track()` method can take up to four arguments. The proper data type and syntax for each are: * **key:** The `key` variable used in the `GetTreatment` call and firing this track event. The expected data type is **String**. -* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in Harness FME. * **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: * Contains 63 characters or fewer. * Starts with a letter or number. * Contains only letters, numbers, hyphen, underscore, or period. * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` * **VALUE:** (Optional) The value to be used in creating the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is **Integer** or **Double**. -* **PROPERTIES:** (Optional) A map of key value pairs that can be used to filter your metrics. Learn more about event property capture [in the Events guide](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties). Split currently supports three types of properties: strings, numbers, and booleans. +* **PROPERTIES:** (Optional) A map of key value pairs that can be used to filter your metrics. Learn more about event property capture [in the Events guide](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties). FME currently supports three types of properties: strings, numbers, and booleans. -The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK can successfully queue the event to be sent back to Split's servers on the next event post. The SDK returns `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method has been provided. +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK can successfully queue the event to be sent back to Harness servers on the next event post. The SDK returns `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method has been provided. In case a bad input is provided, refer to the [Events](https://help.split.io/hc/en-us/articles/360020585772-Track-events) guide for more information about our SDK's expected behavior. @@ -462,19 +462,19 @@ The SDK has a number of knobs for configuring performance. Each knob is tuned to | **Configuration** | **Description** | **Default value** | | --- | --- | --- | -| FeaturesRefreshRate | The SDK polls Split servers for changes to feature flags at this rate (in seconds). | 5 seconds | -| SegmentsRefreshRate | The SDK polls Split servers for changes to segments at this rate (in seconds). | 60 seconds | -| ImpressionsRefreshRate | The treatment log captures which customer saw what treatment (on, off, etc) at what time. This log is periodically flushed back to Split servers. This configuration controls how quickly the cache expires after a write (in seconds). | 30 seconds | -| TelemetryRefreshRate | The SDK caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers (in seconds). | 3600 seconds | +| FeaturesRefreshRate | The SDK polls Harness servers for changes to feature flags at this rate (in seconds). | 5 seconds | +| SegmentsRefreshRate | The SDK polls Harness servers for changes to segments at this rate (in seconds). | 60 seconds | +| ImpressionsRefreshRate | The treatment log captures which customer saw what treatment (on, off, etc) at what time. This log is periodically flushed back to Harness servers. This configuration controls how quickly the cache expires after a write (in seconds). | 30 seconds | +| TelemetryRefreshRate | The SDK caches diagnostic data that it periodically sends to Harness servers. This configuration controls how frequently this data is sent back to Harness servers (in seconds). | 3600 seconds | | ConnectionTimeout | HTTP client connection timeout (in ms). | 15000ms | | ReadTimeout | HTTP socket read timeout (in ms). | 15000ms | -| LabelsEnabled | Enable/disable labels from being sent to the Split backend. Labels may contain sensitive information. | true | -| EventsFirstPushWindow | The SDK collects the events generated by the customer. This setting controls the number of seconds to wait for sending the events to the Split servers (in seconds) after the SDK is built. | 10 seconds | -| EventsPushRate | The SDK collects the events generated by the customer. This setting controls how frequently the events are sent to the Split servers (in seconds) after the first push. | 60 seconds | -| EventsQueueSize | The SDK collects the events generated by the customer. This setting controls how many events are stored locally before sending them to the Split servers (for standalone mode only). | 500 | -| IPAddressesEnabled | Disable machine IP and Hostname from being sent to Split backend. IP and Hostname may contain sensitive information. | true | +| LabelsEnabled | Enable/disable labels from being sent to the Harness servers. Labels may contain sensitive information. | true | +| EventsFirstPushWindow | The SDK collects the events generated by the customer. This setting controls the number of seconds to wait for sending the events to the Harness servers (in seconds) after the SDK is built. | 10 seconds | +| EventsPushRate | The SDK collects the events generated by the customer. This setting controls how frequently the events are sent to the Harness servers (in seconds) after the first push. | 60 seconds | +| EventsQueueSize | The SDK collects the events generated by the customer. This setting controls how many events are stored locally before sending them to the Harness servers (for standalone mode only). | 500 | +| IPAddressesEnabled | Disable machine IP and Hostname from being sent to Harness servers. IP and Hostname may contain sensitive information. | true | | StreamingEnabled | Boolean flag to enable the streaming service as default synchronization mechanism. In the event of an issue with streaming, the SDK will fallback to the polling mechanism. If false, the SDK will poll for changes as usual without attempting to use streaming. | true | -| ImpressionsMode | Defines how impressions are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Split; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, all impressions are queued and sent to Split; this is useful for validations. Use DEBUG mode when you want every impression to be logged in Split user interface when trying to debug your SDK setup. This setting does not impact the impression listener which receives all generated impressions locally. | Optimized | +| ImpressionsMode | Defines how impressions are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Harness; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Harness FME and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, all impressions are queued and sent to Harness; this is useful for validations. Use DEBUG mode when you want every impression to be logged user interface when trying to debug your SDK setup. This setting does not impact the impression listener which receives all generated impressions locally. | Optimized | | ProxyHost | The name of the proxy host. | string.empty | | ProxyPort | The port number on Host to use. | 0 (not set) | | FlagSetsFilter | This setting allows the SDK to only synchronize the feature flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the SDK instance, bringing all the benefits from a reduced payload. | null | @@ -506,11 +506,11 @@ catch (Exception ex) **Configuring this Redis integration section is optional for most setups. Read below to determine if it might be useful for your project** -By default, the Split client stores the state it needs to compute treatments (rollout plans, segments, and so on) in memory. As a result, it is easy to get set up with Split: instantiate a client and start using it. +By default, the SDK factory client stores the state it needs to compute treatments (rollout plans, segments, and so on) in memory. As a result, it is easy to get set up with FME: instantiate a client and start using it. -This simplicity hides one important detail that is worth exploring. Because each Split client downloads and stores state separately, a change in a feature flag is picked up by every client on its own schedule. If a customer issues back-to-back requests that are served by two different machines behind a load balancer, the customer can see different treatments for the same feature flag because one Split client may not have picked up the latest change. This drift in clients is natural and usually ignorable as long as each client sets an aggressive value for `FeaturesRefreshRate` and `SegmentsRefreshRate`. You can learn more about setting these rates in the [Configuration section](#configuration) below. +This simplicity hides one important detail that is worth exploring. Because each SDK factory client downloads and stores state separately, a change in a feature flag is picked up by every client on its own schedule. If a customer issues back-to-back requests that are served by two different machines behind a load balancer, the customer can see different treatments for the same feature flag because one SDK factory client may not have picked up the latest change. This drift in clients is natural and usually ignorable as long as each client sets an aggressive value for `FeaturesRefreshRate` and `SegmentsRefreshRate`. You can learn more about setting these rates in the [Configuration section](#configuration) below. -However, if your application requires a total guarantee that Split clients across your entire infrastructure pick up a change in a feature flag at the exact same time, then the only way to ensure that is to externalize the state of the Split client in a data store hosted on your infrastructure. +However, if your application requires a total guarantee that SDK clients across your entire infrastructure pick up a change in a feature flag at the exact same time, then the only way to ensure that is to externalize the state of the SDK factory client in a data store hosted on your infrastructure. We currently support Redis for this external data store. @@ -618,7 +618,7 @@ private bool CertificateValidation(object sender, X509Certificate certificate, X ### Redis cluster support -The Split .NET SDK version **7.10.0 and above** supports Redis with [Cluster](https://redis.io/topics/cluster-spec). +The FME .NET SDK version **7.10.0 and above** supports Redis with [Cluster](https://redis.io/topics/cluster-spec). To initiate the SDK with support for Redis Cluster, use the following code snippet: @@ -669,7 +669,7 @@ You should use the same KeyHashTag value in the [Split Synchronizer](https://hel ## Localhost mode -For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the Split SDK can be started in **localhost** mode (aka off-the-grid mode). In this mode, the SDK neither polls nor updates Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the feature flags. +For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the SDK can be started in **localhost** mode (aka off-the-grid mode). In this mode, the SDK neither polls nor updates Harness servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the feature flags. To use the SDK in localhost mode, replace the SDK Key with "localhost", as shown in the example below: @@ -725,7 +725,7 @@ catch (Exception ex) } ``` -Split SDK maintains backward compatibility by the legacy file (.split), now deprecated. +The SDK maintains backward compatibility by the legacy file (.split), now deprecated. ```csharp title="C#" var config = new ConfigurationOptions @@ -762,7 +762,7 @@ new-navigation v3 ## Manager -Use the Split Manager to get a list of feature flags available to the Split client. +Use the Split Manager to get a list of feature flags available to the SDK factory client. To instantiate a Manager in your code base, use the same factory that you used for your client. @@ -844,7 +844,7 @@ public class SplitView ## Listener -Split SDKs send impression data back to Split servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an impression listener. +FME SDKs send impression data back to Harness servers periodically and as a result of evaluating feature flags. To additionally send this information to a location of your choice, define and attach an impression listener. The SDK sends the generated impressions to the impression listener right away. @@ -918,7 +918,7 @@ Common.Logging.LogManager.Adapter = new MyAdapter(); The .NET Core SDK uses [Microsoft.Extensions.Logging](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging?view=dotnet-plat-ext-3.0) that works well with a variety of built-in and third-party logging providers. -The following example shows how to include the Split SDK logs in only one line by updating your Startup class. +The following example shows how to include the SDK logs in only one line by updating your Startup class. ```csharp title="C#" //... @@ -958,7 +958,7 @@ log4net 2.0.12 To use this example, do the following: 1. [Download the project](https://github.com/splitio/split-dotnet-debug-log-examples/tree/main/SplitLog4netExample_NETCore) and open the project from Visual Studio. -2. Open the SplitInitializer.cs file and replace the "SDK API KEY" text with the server side SDK KEY. +2. Open the SplitInitializer.cs file and replace the "SDK API KEY" text with the server-side SDK KEY. 3. Optionally edit the log4net.config file to change the log file path, name or format. ### Enable debug logging using Log4net library @@ -974,7 +974,7 @@ log4net 2.0.8 To use this example, do the following: 1. [Download the project](https://github.com/splitio/split-dotnet-debug-log-examples/tree/main/SplitLog4netExample_NETFramework) and open the project from Visual Studio. -2. Open the Program.cs file and update the apikey variable with the server side SDK KEY. +2. Open the Program.cs file and update the apikey variable with the server-side SDK KEY. 3. Optionally edit the log4net.config file to change the log file path, name, or format. ### Custom logging diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/nodejs-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/nodejs-sdk.md index bd8aa3c6f6c..2d938423fd1 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/nodejs-sdk.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/nodejs-sdk.md @@ -20,7 +20,7 @@ The JavaScript SDK supports Node.js version 14.x or later. ## Initialization -Set up Split in your code base with two simple steps. +Set up FME in your code base with two simple steps. ### 1. Import the SDK into your project @@ -34,7 +34,7 @@ npm install --save @splitsoftware/splitio Since version 2.0.0 of the split-synchronizer, we use a more efficient scheme to store impressions in Redis. This approach is faster and easier on your Redis instances, since it yields better throughput of impressions to the backend. If you use this SDK with the Synchronizer in Redis or Proxy mode, you will need the newest versions of our Split Synchronizer. It is recommended that once you're using SDK versions compatible with Split-Sync 2.0 on all your applications pointing to the redis instance maintained by the Split-Sync, you disable backwards compatibility (this is as easy as changing a parameter to `true` on the JSON config or an environment variable to `on` if you're using the docker image). ::: -### 2. Instantiate the SDK and create a new Split client +### 2. Instantiate the SDK and create a new SDK factory client @@ -68,7 +68,7 @@ const client: SplitIO.IClient = factory.client(); :::warning **Updating to Node.js SDK version 11** -While Split Node.js SDK previously supported Node.js v6 and above, the SDK now requires Node.js v14 or above. +While FME Node.js SDK previously supported Node.js v6 and above, the SDK now requires Node.js v14 or above. **Updating to Node.js SDK version 10** @@ -85,23 +85,23 @@ With the SDK package on NPM, you get the SplitIO namespace, which contains usefu Feel free to dive in to the declaration files if IntelliSense is not enough! ::: -We recommend instantiating the Split factory once as a singleton and reusing it throughout your application. +We recommend instantiating the SDK factory once as a singleton and reusing it throughout your application. -Configure the SDK with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split user interface, on your Admin settings page, API keys section. Select a server-side SDK API key. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. +Configure the SDK with the SDK key for the FME environment that you would like to access. The SDK key is available in Harness FME Admin settings. Select a server-side SDK API key. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. -Use the Split client to evaluate treatments. +Use the SDK factory client to evaluate treatments. ## Using the SDK ### Basic use -When the SDK is instantiated, it kicks off background jobs to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds. While the SDK is in this intermediate state, if it is asked to evaluate which treatment to show to the logged in customer for a specific feature flag, it may not have data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). +When the SDK is instantiated, it kicks off background jobs to update an in-memory cache with small amounts of data fetched from Harness servers. This process can take up to a few hundred milliseconds. While the SDK is in this intermediate state, if it is asked to evaluate which treatment to show to the logged in customer for a specific feature flag, it may not have data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). To make sure the SDK is properly loaded before asking it for a treatment, block until the SDK is ready (as shown below). We set the client to listen for the `SDK_READY` event triggered by the SDK before asking for an evaluation. When the `SDK_READY` event fires, you can use the `getTreatment` method to return the proper treatment based on the `key` and `FEATURE_FLAG_NAME` attributes you provided. -Then use an if-else-if block as shown below and insert the code for the different treatments that you defined in the Split user interface. Remember the final else branch in your code to handle the client returning the [control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). +Then use an if-else-if block as shown below and insert the code for the different treatments that you defined in Harness FME. Remember the final else branch in your code to handle the client returning the [control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). @@ -143,7 +143,7 @@ client.on(client.Event.SDK_READY, () => { To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK's `getTreatment` method needs to be passed an attribute map at runtime. -In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in the Split user interface to decide whether to show the `on` or `off` treatment to this account. +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `getTreatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in Harness FME to decide whether to show the `on` or `off` treatment to this account. The `getTreatment` method has a number of variations that are described below. Each of these additionally has a variation that takes an attributes argument, which can defines attributes of the following types: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: @@ -212,7 +212,7 @@ You can pass your attributes in the same way to the `client.getTreatments` metho ### Multiple evaluations at once -In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` from the Split client to do this. +In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of `getTreatments` from the SDK factory client to do this. * `getTreatments`: Pass a list of the feature flag names you want treatments for. * `getTreatmentsByFlagSet`: Evaluate all flags that are part of the provided set name and are cached on the SDK instance. * `getTreatmentsByFlagSets`: Evaluate all flags that are part of the provided set names and are cached on the SDK instance. @@ -274,7 +274,7 @@ To [leverage dynamic configurations with your treatments](https://help.split.io/ This method will return an object containing the treatment and associated configuration. -The config element will be a stringified version of the configuration JSON defined in the Split user interface. If there is no configuration defined for a treatment, the SDK will return `null` for the config parameter. +The config element will be a stringified version of the configuration JSON defined in Harness FME. If there is no configuration defined for a treatment, the SDK will return `null` for the config parameter. This method takes the exact same set of arguments as the standard `getTreatment` method. See below for examples on proper usage: @@ -348,7 +348,7 @@ treatmentResults = client.getTreatmentsWithConfigByFlagSets('user_id', flagSets) ### Shutdown -Call the `client.destroy()` method before letting a process using the SDK exit, as this method gracefully shuts down the Split SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. +Call the `client.destroy()` method before letting a process using the SDK exit, as this method gracefully shuts down the SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. @@ -367,7 +367,7 @@ A call to the `destroy()` method also destroys the factory object. When creating ## Track -Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users’ actions and metrics. +Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to and allows you to measure the impact of your feature flags on your users’ actions and metrics. [Learn more](https://help.split.io/hc/en-us/articles/360020585772) about using track events in features. @@ -381,9 +381,9 @@ In the examples below, you can see that the `.track()` method can take up to fiv * Contains only letters, numbers, hyphen, underscore, or period. * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` * **VALUE:** (Optional) The value to be used in creating the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is **Integer** or **Float**. -* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. +* **PROPERTIES:** (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. FME currently supports three types of properties: strings, numbers, and booleans. -The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event to be sent back to Split's servers on the next event post. The SDK will return `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method has been provided. +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event to be sent back to Harness servers on the next event post. The SDK will return `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method has been provided. In the case that a bad input has been provided, you can read more about our SDK's expected behavior [here](https://help.split.io/hc/en-us/articles/360020585772-Track-events) @@ -451,22 +451,22 @@ The SDK has a number of knobs for configuring performance. Each knob is tuned to | **Configuration** | **Description** | **Default value** | | --- | --- | --- | -| core.labelsEnabled | Disable labels from being sent to the Split backend. Labels may contain sensitive information. | true | -| core.IPAddressesEnabled | Disable machine IP and Hostname from being sent to Split backend. IP and Hostname may contain sensitive information. | true | +| core.labelsEnabled | Disable labels from being sent to the Harness servers. Labels may contain sensitive information. | true | +| core.IPAddressesEnabled | Disable machine IP and Hostname from being sent to Harness servers. IP and Hostname may contain sensitive information. | true | | startup.readyTimeout | Maximum amount of time in seconds to wait before notifying a timeout. Zero means no timeout, so no `SDK_READY_TIMED_OUT` event is fired. | 15 | | startup.requestTimeoutBeforeReady | Time to wait for a request before the SDK is ready. If this time expires, Node.js SDK tries again `retriesOnFailureBeforeReady` times before notifying its failure to be `ready`. Zero means no timeout. | 15 | | startup.retriesOnFailureBeforeReady | Number of quick retries we do while starting up the SDK. | 1 | -| scheduler.featuresRefreshRate | The SDK polls Split servers for changes to feature rollout plans. This parameter controls this polling period in seconds. | 60 | -| scheduler.segmentsRefreshRate | The SDK polls Split servers for changes to segment definitions. This parameter controls this polling period in seconds. | 60 | -| scheduler.impressionsRefreshRate | The SDK sends information on who got what treatment at what time back to Split servers to power analytics. This parameter controls how often this data is sent to Split servers. The parameter should be in seconds. | 300 | +| scheduler.featuresRefreshRate | The SDK polls Harness servers for changes to feature rollout plans. This parameter controls this polling period in seconds. | 60 | +| scheduler.segmentsRefreshRate | The SDK polls Harness servers for changes to segment definitions. This parameter controls this polling period in seconds. | 60 | +| scheduler.impressionsRefreshRate | The SDK sends information on who got what treatment at what time back to Harness servers to power analytics. This parameter controls how often this data is sent to Harness servers. The parameter should be in seconds. | 300 | | scheduler.impressionsQueueSize | The max amount of impressions we queue. If the queue is full, the SDK flushes the impressions and resets the timer. | 30000 | -| scheduler.eventsPushRate | The SDK sends tracked events to Split servers. This setting controls that flushing rate in seconds. | 60 | +| scheduler.eventsPushRate | The SDK sends tracked events to Harness servers. This setting controls that flushing rate in seconds. | 60 | | scheduler.eventsQueueSize | The max amount of events we queue. If the queue is full, the SDK flushes the events and resets the timer. | 500 | -| scheduler.telemetryRefreshRate | The SDK caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers (in seconds). | 3600 seconds (1 hour) | +| scheduler.telemetryRefreshRate | The SDK caches diagnostic data that it periodically sends to Harness servers. This configuration controls how frequently this data is sent back to Harness servers (in seconds). | 3600 seconds (1 hour) | | sync.splitFilters | Filter specific feature flags to be synced and evaluated by the SDK. This is formed by a type string property and a list of string values for the given criteria. Using the types 'bySet' (recommended, flag sets are available in all tiers) or 'byName', pass an array of strings defining the query. If empty or unset, all feature flags are downloaded by the SDK. | [] | -| sync.impressionsMode | This configuration defines how impressions (decisioning events) are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Split; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Split and only minimum viable data to support usage stats is tracked, so never use this mode if you are experimenting with instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions' network and storage load. In DEBUG mode, ALL impressions are queued and sent to Split; this is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. Keep in mind that both the OPTIMIZED and DEBUG modes utilize an internal cache which uses heap memory incrementally up to a maximum limit ___without a memory leak___. | OPTIMIZED | -| sync.enabled | Controls the SDK continuous synchronization flags. When `true`, a running SDK processes rollout plan updates performed in the Split user interface (default). When `false`, it fetches all data upon init, which ensures a consistent experience during a user session and optimizes resources when these updates are not consumed by the app. | true | -| sync.requestOptions.agent | A custom Node.js HTTP(S) Agent used to perform the requests to the Split servers. See [Proxy](#proxy) for details. | undefined | +| sync.impressionsMode | This configuration defines how impressions (decisioning events) are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Harness; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Harness FME and only minimum viable data to support usage stats is tracked, so never use this mode if you are experimenting with instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions' network and storage load. In DEBUG mode, ALL impressions are queued and sent to Harness; this is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally. Keep in mind that both the OPTIMIZED and DEBUG modes utilize an internal cache which uses heap memory incrementally up to a maximum limit ___without a memory leak___. | OPTIMIZED | +| sync.enabled | Controls the SDK continuous synchronization flags. When `true`, a running SDK processes rollout plan updates performed in Harness FME (default). When `false`, it fetches all data upon init, which ensures a consistent experience during a user session and optimizes resources when these updates are not consumed by the app. | true | +| sync.requestOptions.agent | A custom Node.js HTTP(S) Agent used to perform the requests to the Harness servers. See [Proxy](#proxy) for details. | undefined | | sync.requestOptions.getHeaderOverrides | A callback function that can be used to override the Authentication header or append new headers to the SDK's HTTP(S) requests. | undefined | | storage.type | Storage type to be used by the SDK. Possible values are `MEMORY`, and `REDIS`. | `MEMORY` | | storage.options | Options to be passed to the storage instance. Only usable with `REDIS` type storage for now. See [Redis configuration](#redis-configuration) for details. | {}
No default options | @@ -574,11 +574,11 @@ const factory: SplitIO.ISDK = SplitFactory({ **Configuring this Redis integration section is optional for most setups. Read below to determine if it might be useful for your project.** -By default, the Split client stores the state it needs to compute treatments (rollout plans, segments, and so on) in memory. As a result, it is easy to get set up with Split: simply instantiate a client and start using it. +By default, the SDK factory client stores the state it needs to compute treatments (rollout plans, segments, and so on) in memory. As a result, it is easy to get set up with FME: simply instantiate a client and start using it. -This simplicity hides one important detail that is worth exploring. Because each Split client downloads and stores state separately, a change in a feature flag is picked up by every client on its own schedule. Thus, if a customer issues back-to-back requests that are served by two different machines behind a load balancer, the customer can see different treatments for the same feature flag because one Split client may not have picked up the latest change. This drift in clients is natural and usually ignorable as long as each client sets an aggressive value for `FeaturesRefreshRate` and `SegmentsRefreshRate`. You can learn more about setting these rates in the [Configuration section](#configuration) below. +This simplicity hides one important detail that is worth exploring. Because each SDK factory client downloads and stores state separately, a change in a feature flag is picked up by every client on its own schedule. Thus, if a customer issues back-to-back requests that are served by two different machines behind a load balancer, the customer can see different treatments for the same feature flag because one SDK factory client may not have picked up the latest change. This drift in clients is natural and usually ignorable as long as each client sets an aggressive value for `FeaturesRefreshRate` and `SegmentsRefreshRate`. You can learn more about setting these rates in the [Configuration section](#configuration) below. -However, if your application requires a total guarantee that Split clients across your entire infrastructure pick up a change in a feature flag at the exact same time or you need an async data store, then the only way to ensure that is to externalize the state of the Split client in a data store hosted on your infrastructure. +However, if your application requires a total guarantee that SDK clients across your entire infrastructure pick up a change in a feature flag at the exact same time or you need an async data store, then the only way to ensure that is to externalize the state of the SDK factory client in a data store hosted on your infrastructure. We currently support Redis for this external data store. @@ -592,7 +592,7 @@ Follow the steps in our [Split Synchronizer](https://help.split.io/hc/en-us/arti In consumer mode, a client can be embedded in your application code and respond to calls to `getTreatment` by retrieving state from the data store (Redis in this case). -Here is how to configure and get treatments for a Split client in consumer mode. +Here is how to configure and get treatments for a SDK factory client in consumer mode. @@ -703,7 +703,7 @@ The SDK in consumer mode connects to Redis to function, using URL `redis://local ## Localhost mode -For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the Split SDK can be started in **localhost** mode (aka off-the-grid mode). In this mode, the SDK neither polls nor updates Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the feature flags. +For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the SDK can be started in **localhost** mode (aka off-the-grid mode). In this mode, the SDK neither polls nor updates Harness servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the feature flags. To use the SDK in localhost mode, set the `authorizationKey` config property to "localhost", as shown in the example below: @@ -814,7 +814,7 @@ In addition, there are some extra configuration parameters that can be used when ## Manager -Use the Split Manager to get a list of feature flags available to the Split client. +Use the Split Manager to get a list of feature flags available to the SDK factory client. To instantiate a Manager in your code base, use the same factory that you used for your client. @@ -926,7 +926,7 @@ type SplitView = { ## Listener -Split SDKs send impression data back to Split servers periodically when evaluating feature flags. To send this information to a location of your choice, define and attach an *impression listener*. Use the SDK's `impressionListener` parameter, where you can add an implementation of `ImpressionListener`. This implementation **must** define the `logImpression` method. It receives data in the following schema. +FME SDKs send impression data back to Harness servers periodically when evaluating feature flags. To send this information to a location of your choice, define and attach an *impression listener*. Use the SDK's `impressionListener` parameter, where you can add an implementation of `ImpressionListener`. This implementation **must** define the `logImpression` method. It receives data in the following schema. | **Name** | **Type** | **Description** | | --- | --- | --- | @@ -1056,7 +1056,7 @@ For more information on using the logging framework in SDK versions prior to 9.2 ## Proxy -If you need to use a network proxy, you can provide a custom [Node.js HTTPS Agent](https://nodejs.org/api/https.html#class-httpsagent) by setting the `sync.requestOptions.agent` configuration variable. The SDK will use this agent to perform requests to Split servers. +If you need to use a network proxy, you can provide a custom [Node.js HTTPS Agent](https://nodejs.org/api/https.html#class-httpsagent) by setting the `sync.requestOptions.agent` configuration variable. The SDK will use this agent to perform requests to Harness servers. diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/php-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/php-sdk.md index da6bf635a33..f4df305fe41 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/php-sdk.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/php-sdk.md @@ -24,13 +24,13 @@ The PHP SDK supports PHP language version 7.3 and later. The PHP SDK is architected differently from our other SDKs. This is because of the **share nothing** nature of PHP, which means that you need to leverage a remote data store that your processes can share to ensure that your customers are served consistent treatments from our SDK. The SDK has three components. -#### SDK client +#### SDK factory client -The SDK client is embedded within your PHP app. It decides which treatment to show to a customer for a particular feature flag. +The SDK factory client is embedded within your PHP app. It decides which treatment to show to a customer for a particular feature flag. #### Split Synchronizer -The Split Synchronizer service fetches data from the Split servers so it can evaluate what treatment to show to a customer. This is a background service that can run on one machine on a schedule via your scheduling system. Refer to the [Split Synchronizer documentation](https://help.split.io/hc/en-us/articles/360019686092) for more information. +The Split Synchronizer service fetches data from the Harness servers so it can evaluate what treatment to show to a customer. This is a background service that can run on one machine on a schedule via your scheduling system. Refer to the [Split Synchronizer documentation](https://help.split.io/hc/en-us/articles/360019686092) for more information. #### Cache @@ -54,13 +54,13 @@ Since version 2.0.0 of the split-synchronizer, we use a more efficient scheme to When the composer is done, follow the steps in our [Split Synchronizer](https://help.split.io/hc/en-us/articles/360019686092) documents to get everything set to sync data to your Redis cache. After you do that, come back to set up the SDK in consumer mode! -### 3. Instantiate the SDK and create a new split client +### 3. Instantiate the SDK and create a new SDK factory client -We recommend instantiating the Split factory once as a singleton and reusing it throughout your application. +We recommend instantiating the SDK factory once as a singleton and reusing it throughout your application. Use the code snippet below to instantiate the client in your code base. You need to provide your Redis details and your SDK API key. -Configure the SDK with the SDK API key for the Split environment that you would like to access. The SDK key is available in the Split user interface, on your Admin settings page, API keys section. Select a server-side SDK API key. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. +Configure the SDK with the SDK API key for the FME environment that you would like to access. The SDK key is available in Harness FME Admin settings. Select a server-side SDK API key. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. Do all of this as a part of the startup sequence of your application. @@ -90,9 +90,9 @@ $splitClient = $splitFactory->client(); ### Basic use -After you instantiate the SDK client, use the `getTreatment` method of the SDK client to decide what version of your feature flags your customers are served. The method requires the `FEATURE_FLAG_NAME` attribute that you want to ask for a treatment and a unique `key` attribute that corresponds to the end user that you want to serve the feature flag to. +After you instantiate the SDK factory client, use the `getTreatment` method of the SDK factory client to decide what version of your feature flags your customers are served. The method requires the `FEATURE_FLAG_NAME` attribute that you want to ask for a treatment and a unique `key` attribute that corresponds to the end user that you want to serve the feature flag to. -From there, you need to use an if-else-if block as shown below and insert the code for the different treatments that you defined in the Split user interface. Remember the final else branch in your code to handle the client returning [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). +From there, you need to use an if-else-if block as shown below and insert the code for the different treatments that you defined in Harness FME. Remember the final else branch in your code to handle the client returning [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). ```php title="PHP" `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` * **VALUE:** (Optional) The value to be used in creating the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is **Integer** or **Float**. -* **PROPERTIES:** (Optional) An Map of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. +* **PROPERTIES:** (Optional) An Map of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. FME currently supports three types of properties: strings, numbers, and booleans. :::warning[Redis Support] If you are using our SDK with Redis, you need Split Synchronizer v2.3.0 version at least in order to support *properties* in the `track` method. ::: -The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event to be sent back to Split's servers on the next event post. The SDK will return `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method has been provided. +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event to be sent back to Harness servers on the next event post. The SDK will return `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method has been provided. In the case that a bad input has been provided, you can read more about our SDK's expected behavior in the [Events documentation](https://help.split.io/hc/en-us/articles/360020585772-Track-events) @@ -298,7 +298,7 @@ With the SDK architecture, there is a set of options that you can configure to g | log | Configure the log adapter and level. Refer to the [Logging](#logging) section. | | cache | Configure the Redis cache adapter. Refer to the [Redis cache](#redis-cache) section. | | impressionListener | Instance of an impression listener to send impression data to a custom location. | -| IPAddressesEnabled | Flag to disable IP addresses and host name from being sent to the Split backend. | +| IPAddressesEnabled | Flag to disable IP addresses and host name from being sent to the Harness servers. | ```php title="PHP" client(); ## Localhost mode -For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the Split SDK can be started in **localhost** mode (aka off-the-grid mode). In this mode, the SDK neither polls nor updates Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the features. +For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the SDK can be started in **localhost** mode (aka off-the-grid mode). In this mode, the SDK neither polls nor updates Harness servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the features. To use the SDK in localhost mode, replace the SDK Key with "localhost", as shown in the example below: @@ -508,7 +508,7 @@ In the example above, we have 3 entries: ## Manager -Use the Split Manager to get a list of feature flags available to the Split client. +Use the Split Manager to get a list of feature flags available to the SDK factory client. To instantiate a Manager in your code base, use the same factory that you used for your client. @@ -566,7 +566,7 @@ class SplitView ## Listener -Split SDKs send impression data back to Split servers periodically when evaluating feature flags. To send this information to a location of your choice, define and attach an *impression listener*. Use the SDK's `impressionListener` parameter, where you can add an implementation of `ImpressionListener`. This implementation **must** define the `logImpression` method. It receives data in the following schema. +FME SDKs send impression data back to Harness servers periodically when evaluating feature flags. To send this information to a location of your choice, define and attach an *impression listener*. Use the SDK's `impressionListener` parameter, where you can add an implementation of `ImpressionListener`. This implementation **must** define the `logImpression` method. It receives data in the following schema. | **Name** | **Type** | **Description** | | --- | --- | --- | @@ -607,7 +607,7 @@ $splitClient = $splitFactory->client(); ## Logging -The Split SDK provides a custom logger that implements the [PSR-3 standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md). **By default, the SDK logs to syslog and a WARNING log level.** To configure the logger, set the adapter and the desired log level. +The SDK provides a custom logger that implements the [PSR-3 standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md). **By default, the SDK logs to syslog and a WARNING log level.** To configure the logger, set the adapter and the desired log level. :::warning[Production environments] For production environments, we strongly recommend setting the adapter to **syslog** and avoid using the `echo` adapter. Even better, set your own custom adapter. See [Custom logging](#custom-logging) below. @@ -627,7 +627,7 @@ The log configuration parameters are described below. | **Configuration** | **Description** | **Default value** | | --- | --- | --- | -| adapter | The logger adapter. Split SDK supports:
  • **stdout:** Write log messages to standard output (php://stdout)
  • **syslog:** Generate a log message that is distributed by the system logger.
  • **void:** Prevent log writes.
  • **echo:** Echo messages to output. Note that the output could be the web browser.
| syslog | +| adapter | The logger adapter. The SDK supports:
  • **stdout:** Write log messages to standard output (php://stdout)
  • **syslog:** Generate a log message that is distributed by the system logger.
  • **void:** Prevent log writes.
  • **echo:** Echo messages to output. Note that the output could be the web browser.
| syslog | | level | The log level message. According the [PSR-3 standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md) the supported levels are:
  • emergency
  • alert
  • critical
  • error
  • warning
  • notice
  • info
  • debug
| warning | | psr3-instance | Your custom logger instance that implements the [PSR-3 standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md) | null @@ -646,7 +646,7 @@ $options = [ 'log' => ['psr3-instance' => $psrLogger], ]; -/** Create the Split client instance. */ +/** Create the SDK factory client instance. */ $splitFactory = \SplitIO\Sdk::factory('YOUR_SDK_KEY', $options); $splitClient = $splitFactory->client(); ``` @@ -667,6 +667,6 @@ $options = [ 'log' => ['psr3-instance' => $psrLogger], ]; -/** Create the Split Client instance. */ +/** Create the client instance. */ $splitClient = \SplitIO\Sdk::factory('YOUR_SDK_KEY', $options); ``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/php-thin-client-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/php-thin-client-sdk.md index b499d9c77a6..9592372e5b7 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/php-thin-client-sdk.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/php-thin-client-sdk.md @@ -20,7 +20,7 @@ The PHP Thin SDK supports PHP language version 7.3 and later. ## Architecture -The PHP Thin SDK depends on the [Split Daemon (splitd)](https://help.split.io/hc/en-us/articles/18305269686157) which should be set up on the same host. The PHP Thin SDK client uses splitd to maintain the local cached copy of the Split rollout plan and return feature flag evaluations. +The PHP Thin SDK depends on the [Split Daemon (splitd)](https://help.split.io/hc/en-us/articles/18305269686157) which should be set up on the same host. The PHP Thin SDK factory client uses splitd to maintain the local cached copy of the FME definitions and return feature flag evaluations. ## Initialization @@ -36,9 +36,9 @@ The public release of the PHP Thin SDK is available at [packagist.org](https://p When the composer is done, follow the guidance of our [Split Daemon (splitd)](https://help.split.io/hc/en-us/articles/18305269686157) doc to integrate splitd into your application infrastructure. -### 3. Instantiate the SDK and create a new split client +### 3. Instantiate the SDK and create a new SDK factory client -We recommend instantiating the Split factory once as a singleton and reusing it throughout your application. +We recommend instantiating the SDK factory once as a singleton and reusing it throughout your application. ```php title="PHP" client(); ### Basic use -After you instantiate the SDK client, you can start using the `getTreatment` method of the SDK client to decide what version of your features your customers are served. The method requires the `FEATURE_FLAG_NAME` attribute that you want to ask for a treatment and a unique `key` attribute that corresponds to the end user that you want to serve the feature to. +After you instantiate the SDK factory client, you can start using the `getTreatment` method of the SDK factory client to decide what version of your features your customers are served. The method requires the `FEATURE_FLAG_NAME` attribute that you want to ask for a treatment and a unique `key` attribute that corresponds to the end user that you want to serve the feature to. -From there, you simply need to use an if-else-if block as shown below and insert the code for the different treatments that you defined in the Split user interface. Remember the final else branch in your code to handle the client returning [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). +From there, you simply need to use an if-else-if block as shown below and insert the code for the different treatments that you defined in Harness FME. Remember the final else branch in your code to handle the client returning [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). ```php title="PHP" getTreatmentsWithConfig("KEY", null, ["FEATURE ### Shutdown -Due to the nature of PHP and the way HTTP requests are handled, the client is instantiated on every request and automatically destroyed when the request lifecycle comes to an end. The data is synchronized by an external tool and stored in memory, so the SDK client does not need to invoke any shutdown tasks. +Due to the nature of PHP and the way HTTP requests are handled, the client is instantiated on every request and automatically destroyed when the request lifecycle comes to an end. The data is synchronized by an external tool and stored in memory, so the SDK factory client does not need to invoke any shutdown tasks. ## Track -Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users’ actions and metrics. +Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to and allows you to measure the impact of your feature flags on your users’ actions and metrics. Refer to the [Events](https://help.split.io/hc/en-us/articles/360020585772) documentation for more information about using track events in feature flags. In the examples below you can see that the `.track()` method can take up to five arguments. The proper data type and syntax for each are: * **key:** The `key` variable used in the `getTreatment` call and firing this track event. The expected data type is **String**. -* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in Harness FME. * **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: * Contains 63 characters or fewer. * Starts with a letter or number. * Contains only letters, numbers, hyphen, underscore, or period. * This is the regular expression we use to validate the value:
`[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` * **VALUE:** (Optional) The value to be used in creating the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is **Integer** or **Float**. -* **PROPERTIES:** (Optional) A Map of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. +* **PROPERTIES:** (Optional) A Map of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. FME currently supports three types of properties: strings, numbers, and booleans. -The `track` method returns a boolean value of `true` or `false` to indicate whether or not the event was successfully queued to be sent back to Split's servers on the next event post. The SDK will return `false` if the current queue on the Split Daemon is full or if an incorrect input to the `track` method has been provided. +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the event was successfully queued to be sent back to Harness servers on the next event post. The SDK will return `false` if the current queue on the Split Daemon is full or if an incorrect input to the `track` method has been provided. In the case that a bad input has been provided, you can read more about our SDK's expected behavior in the [Events documentation](https://help.split.io/hc/en-us/articles/360020585772-Track-events) @@ -249,11 +249,11 @@ $splitClient = $splitFactory->client(); ### Impressions data -By default, the SDK sends small amounts of information to the Split backend indicating the reason for each treatment returned from a feature flag. An example would be that a user saw the `on` treatment because they are `in segment all`. +By default, the SDK sends small amounts of information to the Harness servers indicating the reason for each treatment returned from a feature flag. An example would be that a user saw the `on` treatment because they are `in segment all`. ## Manager -Use the Split Manager to get a list of feature flags available to the Split client. +Use the Split Manager to get a list of feature flags available to the SDK factory client. To instantiate a Manager in your code base, use the same factory that you used for your client. @@ -323,7 +323,7 @@ class SplitView ## Listener -Split SDKs send impression data back to Split servers periodically when evaluating feature flags. To send this information to a location of your choice, define an *impression listener*. Use the `impressionListener` parameter, where you can provide an implementation of an `ImpressionListener`. This implementation **must** define the `accept` method, with the signature `public function accept(Impression $impression, ?array $attributes)` and with parameters as defined below. +FME SDKs send impression data back to Harness servers periodically when evaluating feature flags. To send this information to a location of your choice, define an *impression listener*. Use the `impressionListener` parameter, where you can provide an implementation of an `ImpressionListener`. This implementation **must** define the `accept` method, with the signature `public function accept(Impression $impression, ?array $attributes)` and with parameters as defined below. | **Name** | **Type** | **Description** | | --- | --- | --- | @@ -369,7 +369,7 @@ $splitClient = $splitFactory->client(); ## Logging -The Split SDK provides a custom logger that implements the [PSR-3 standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md). **By default, the SDK logs to stdout at the INFO log level.** To configure the logger, set the adapter and the desired log level. +The SDK provides a custom logger that implements the [PSR-3 standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md). **By default, the SDK logs to stdout at the INFO log level.** To configure the logger, set the adapter and the desired log level. :::warning[Production environments] For production environments, we strongly recommend passing a proper `psr-instance` parameter with a PSR3 compliant logger (or custom wrapper for a non-cmpliant one). @@ -411,7 +411,7 @@ $sdkConfig = [ 'logging' => ['psr-instance' => $psrLogger], ]; -/** Create the Split Client instance. */ +/** Create the client instance. */ $splitFactory = \SplitIO\ThinSdk\Factory::withConfig($sdkConfig); $splitClient = $splitFactory->client(); ``` @@ -436,7 +436,7 @@ $sdkConfig = [ 'logging' => ['psr-instance' => $psrLogger], ]; -/** Create the Split Client instance. */ +/** Create the client instance. */ $splitFactory = \SplitIO\ThinSdk\Factory::withConfig($sdkConfig); $splitClient = $splitFactory->client(); ``` \ No newline at end of file diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/python-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/python-sdk.md index fc14878358f..9829a2b3c84 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/python-sdk.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/python-sdk.md @@ -34,7 +34,7 @@ Jump to the setup process for the mode your application is built in: ## Initialization: Multi-threaded mode -Set up Split in your code base with two simple steps. +Set up FME in your code base with two simple steps. ### 1. Import the SDK into your project using pip @@ -42,21 +42,21 @@ Set up Split in your code base with two simple steps. pip install 'splitio_client[cpphash]==10.2.0' ``` -### 2. Instantiate the SDK and create a new split client +### 2. Instantiate the SDK and create a new SDK factory client :::danger[If upgrading an existing SDK - Block until ready changes] Starting in version `8.0.0`, readiness has been migrated to a two part implementation. See below for syntax changes you must make if upgrading your SDK to the newest version. ::: -When the SDK is instantiated in `in-memory` mode, it kicks off background tasks to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while its in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK doesn't fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). +When the SDK is instantiated in `in-memory` mode, it kicks off background tasks to update an in-memory cache with small amounts of data fetched from Harness servers. This process can take up to a few hundred milliseconds depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while its in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK doesn't fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). To make sure the SDK is properly loaded before asking it for a treatment, block until the SDK is ready. Since version `8.0.0` This is done by calling the `.block_until_ready()` method in the factory object. This method also accepts a maximum time (in seconds or fractions of it) to wait until the SDK is ready, or throw an exception in case it's not. -We recommend instantiating the Split factory once as a singleton and reusing it throughout your application. +We recommend instantiating the SDK factory once as a singleton and reusing it throughout your application. -Configure the SDK with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split user interface, on your Admin settings page, API keys section. Select a server-side SDK API key. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. +Configure the SDK with the SDK key for the FME environment that you would like to access. The SDK key is available in Harness FME Admin settings. Select a server-side SDK API key. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. ```python title="Python" from splitio import get_factory @@ -78,7 +78,7 @@ Now you can start asking the SDK to evaluate treatments for your customers. Python's asyncio library had gather lot of attention and support and provides many advantages to multi-threaded programming especially in I/O operations, checkout the [official doc](https://docs.python.org/3/library/asyncio.html) for more info. -Set up Split in your code base with two simple steps. +Set up FME in your code base with two simple steps. ### 1. Import the SDK into your project using pip @@ -86,7 +86,7 @@ Set up Split in your code base with two simple steps. pip install 'splitio_client[cpphash,asyncio]==10.2.0' ``` -### 2. Instantiate the SDK and create a new split client +### 2. Instantiate the SDK and create a new SDK factory client :::danger[asyncio support] Starting in version `10.0.0`, SDK support asyncio library, this required a breaking change to upgrade the python supported version to be 3.7.16 or later. @@ -96,7 +96,7 @@ Starting in version `10.0.0`, SDK support asyncio library, this required a break When using the SDK, regardless if the mode is asyncio or Multi-threaded, all the public SDK API are identical, with only one exception; when initializing the factory. ::: -Similar to Multi-threaded mode, when the SDK is instantiated in `in-memory`, it kicks off background asyncio tasks to update an in-memory cache with small amounts of data fetched from Split servers. To make sure the SDK cache is properly loaded before asking it for a treatment, utilize `block_until_ready()` method. +Similar to Multi-threaded mode, when the SDK is instantiated in `in-memory`, it kicks off background asyncio tasks to update an in-memory cache with small amounts of data fetched from Harness servers. To make sure the SDK cache is properly loaded before asking it for a treatment, utilize `block_until_ready()` method. We recommend instantiating the SDK once as a singleton and reusing it throughout your application. @@ -129,7 +129,7 @@ There are a few extra steps for setting up our SDK with Python in multi-process ### SDK architecture When the application is run in a server that spawns multiple processes (workers) to handle HTTP requests, all of them need to access fetched feature flags and segments as well as queuing up impressions and events. Since processes cannot access each other's memory, using the standalone operation mode will result in several sets of synchronisation tasks (threads) doing the same job (at least one per http worker - possibly more, since workers are often restarted). -To avoid this scenario, the Split.IO SDK for Python supports an alternative operation mode, which uses an external tool called `Split-Synchronizer` and a `redis` cache. Our synchronization tool is responsible for maintaining the split data updated and flushing impressions, events and metrics to the split servers. +To avoid this scenario, the Split.IO SDK for Python supports an alternative operation mode, which uses an external tool called `Split-Synchronizer` and a `redis` cache. Our synchronization tool is responsible for maintaining the FME data updated and flushing impressions, events and metrics to the split servers. If you are using a preforked-type server such as uWSGI or GUnicorn, we also offer a series of methods that can be attached to the server's "post-fork" hooks in order to ensure synchronization runs properly on the worker process after the master is forked. The previously mentioned approaches are described in depth below: @@ -141,7 +141,7 @@ The previously mentioned approaches are described in depth below: Before you get started with the cache, download the correct version of Redis to your machine. Our SDK Redis integration requires a Redis version `2.10.5` or later. Also want to make sure to start your Redis server. Refer to the [Redis documentation](https://redis.io/topics/quickstart) for help. After that, there are a few more steps to set up the cache with Redis. -#### 1. Install the Split SDK into your project +#### 1. Install the SDK into your project Use `pip install` to install the SDK. Note that the package is different for standard Python and for Django, as shown below. @@ -171,11 +171,11 @@ Since version `2.0.0` of the split-synchronizer, we use a more efficient scheme Set up the [Split Synchronizer](https://help.split.io/hc/en-us/articles/360019686092) to sync data to a Redis cache. Follow the steps in the [set up article](https://help.split.io/hc/en-us/articles/360019686092), then come back to this doc and go to step 3 to instantiate the client, below. -#### 3. Instantiate the SDK client with Redis enabled +#### 3. Instantiate the SDK factory client with Redis enabled If you are using Django, there is one extra step to add `django_splitio` to `INSTALLED_APPS` in your Django settings and add a SPLITIO dictionary in the Django settings. Input your own SDK key in for `YOUR_SDK_KEY`. -To instantiate the SDK client, copy and paste the code snippet below into your code base where you want to use Split to roll out your feature flag. Again, note that the syntax is different for standard Python and for Django. +To instantiate the SDK factory client, copy and paste the code snippet below into your code base where you want to use Harness FME to roll out your feature flag. Again, note that the syntax is different for standard Python and for Django. @@ -341,10 +341,10 @@ This functionality is currently not supported for this SDK, but is planned for a ### Preforked client setup -Since version `8.4.0` we added support for running our SDK in standalone mode in preforked multiprocess servers. With this feature you can take advantage of using Split in preforking servers such as GUnicorn or uWSGI and attaching it to the `postfork` hooks. This can yield significant performance improvements in terms of memory in comparison to use lazy-style initialization and greatly reduced evaluation time in comparison to use Redis + Split Synchronizer approach at the expense of CPU and BG network traffic. -There are two main steps for initializating the Split SDK by using hooks: +Since version `8.4.0` we added support for running our SDK in standalone mode in preforked multiprocess servers. With this feature you can take advantage of using FME in preforking servers such as GUnicorn or uWSGI and attaching it to the `postfork` hooks. This can yield significant performance improvements in terms of memory in comparison to use lazy-style initialization and greatly reduced evaluation time in comparison to use Redis + Split Synchronizer approach at the expense of CPU and BG network traffic. +There are two main steps for initializating the SDK by using hooks: 1. `preforkedInitialization`: this is a new configuration option that will tell the SDK that it should initiate the SDK in master mode and it will not start polling nor streaming. -2. `factory.resume()`: this is a new method provided by Split Factory that should be executed on newly forked http worker processes in order to resume synchronisation. +2. `factory.resume()`: this is a new method provided by the factory that should be executed on newly forked http worker processes in order to resume synchronisation. :::warning Preforked client is not supported in asyncio mode. @@ -358,9 +358,9 @@ There are a few extra steps to set up SDK with `postfork` option. 1. Importing the `uwsgidecorators` module for handling hooks. 2. Set `preforkedInitialization` as true in the sdk configs. 3. Add and use the `postfork` decorator. -5. Call `factory.resume()` method to resume Split tasks on each forked child process. +5. Call `factory.resume()` method to resume tasks on each forked child process. -**Note:** Make sure to add the parameter `--enable-threads` to enable multi-threading when starting the UWSGI app server. While Python SDK does support UWSGI app server in process based mode, for the SDK to synchronize with Split cloud, you need to enable the multi-threading option, as the background threads perform the synching task. For example: +**Note:** Make sure to add the parameter `--enable-threads` to enable multi-threading when starting the UWSGI app server. While Python SDK does support UWSGI app server in process based mode, for the SDK to synchronize with Harness FME servers, you need to enable the multi-threading option, as the background threads perform the synching task. For example: ```bash title="Shell" uwsgi --http :8080 --chdir /var/app --wsgi-file ${WSGI_PATH} ${UWSGI_MODULE} --master @@ -410,7 +410,7 @@ INSTALLED_APPS = ( 'preforkedInitialization': True ## Step 2 } ## ------------------------- -## in setup Split module +## in setup FME module from django_splitio import get_factory @@ -452,9 +452,9 @@ For further reading about uwsgi decorators and postfork you can take a look at t ### Basic use -After you instantiate the SDK client, you can start using the `get_treatment` method of the SDK client to decide what version of your feature flags your customers are served. The method requires the `FEATURE_FLAG_NAME` attribute that you want to ask for a treatment and a unique `key` attribute that corresponds to the end user that you want to serve the feature to. +After you instantiate the SDK factory client, you can start using the `get_treatment` method of the SDK factory client to decide what version of your feature flags your customers are served. The method requires the `FEATURE_FLAG_NAME` attribute that you want to ask for a treatment and a unique `key` attribute that corresponds to the end user that you want to serve the feature to. -From there, you simply need to use an if-else-if block as shown below and insert the code for the different treatments that you defined in the Split UI. Remember the final else branch in your code to handle the client returning [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). +From there, you simply need to use an if-else-if block as shown below and insert the code for the different treatments that you defined in Harness FME. Remember the final else branch in your code to handle the client returning [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). @@ -493,7 +493,7 @@ If the `key` attribute is something other than `string`, Python SDK returns `CON To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK's `get_treatment` method needs to be passed an attribute map at runtime. -In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `get_treatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in the Split user interface to decide whether to show the `on` or `off` treatment to this account. +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `get_treatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in Harness FME to decide whether to show the `on` or `off` treatment to this account. The `get_treatment` method supports five types of attributes: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: @@ -560,7 +560,7 @@ loop.run_until_complete(main()) ### Multiple evaluations at once -In some instances, you may want to evaluate treatments for multiple feature flag at once. Use the different variations of `get_treatments` method from the split client to do this. +In some instances, you may want to evaluate treatments for multiple feature flag at once. Use the different variations of `get_treatments` method from the SDK factory client to do this. * `get_treatments`': Pass a list of the feature flag names you want treatments for. * `get_treatments_by_flag_set`: Evaluate all flags that are part of the provided set name and are cached on the SDK instance. * `get_treatments_by_flag_sets`: Evaluate all flags that are part of the provided set names and are cached on the SDK instance. @@ -623,7 +623,7 @@ To [leverage dynamic configurations with your treatments](https://help.split.io/ This method will return an object containing the treatment and associated configuration. -The config element will be a stringified version of the configuration JSON defined in the Split user interface. If there are no configs defined for a treatment, the SDK returns `None` for the config parameter. +The config element will be a stringified version of the configuration JSON defined in Harness FME. If there are no configs defined for a treatment, the SDK returns `None` for the config parameter. This method takes the exact same set of arguments as the standard `get_treatment` method. See below for examples on proper usage: @@ -737,7 +737,7 @@ for feature_flag, treatment_with_config in result.items(): ### Shutdown -The in-memory implementation of Python uses threads in Multi-threaded mode and tasks in asyncio mode to synchronize feature flags, segments, and impressions. If at any point in the application the split client is not longer needed, you can disable it by calling the `destroy()` method on the factory object. +The in-memory implementation of Python uses threads in Multi-threaded mode and tasks in asyncio mode to synchronize feature flags, segments, and impressions. If at any point in the application the SDK factory client is not longer needed, you can disable it by calling the `destroy()` method on the factory object. This does NOT kill the threads or tasks if they are synchronizing, but prevents them from rescheduling for future executions. @@ -769,25 +769,25 @@ A call to the `destroy()` method also destroys the factory object. When creating ## Track -Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users’ actions and metrics. +Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to and allows you to measure the impact of your feature flags on your users’ actions and metrics. [Learn more](https://help.split.io/hc/en-us/articles/360020585772) about using track events in splits. In the examples below you can see that the `.track()` method can take up to five arguments. The proper data type and syntax for each are: * **key:** The `key` variable used in the `get_treatment` call and firing this track event. The expected data type is **String**. -* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in Harness FME. * **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: * Contains 63 characters or fewer. * Starts with a letter or number. * Contains only letters, numbers, hyphen, underscore, or period. * This is the regular expression we use to validate the value:
`[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` * **VALUE:** (Optional) The value to be used in creating the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is **Integer** or **Float**. -* **PROPERTIES:** (Optional) A Map of key value pairs that can be used to filter your metrics. Learn more about event property capture [in the Events guide](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties). Split currently supports three types of properties: strings, numbers, and booleans. +* **PROPERTIES:** (Optional) A Map of key value pairs that can be used to filter your metrics. Learn more about event property capture [in the Events guide](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties). FME currently supports three types of properties: strings, numbers, and booleans. **Redis Support:** If you are using our SDK with Redis, you need Split Synchronizer **2.3.0** version at least in order to support *properties* in the `track` method. -The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event to be sent back to Split's servers on the next event post. The SDK will return `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method has been provided. +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK was able to successfully queue the event to be sent back to Harness servers on the next event post. The SDK will return `false` if the current queue size is equal to the config set by `eventsQueueSize` or if an incorrect input to the `track` method has been provided. In the case that a bad input has been provided, you can read more about our SDK's expected behavior [here](https://help.split.io/hc/en-us/articles/360020585772-Track-events) @@ -852,14 +852,14 @@ The SDK has a number of knobs for configuring performance. Each knob is tuned to | **Configuration** | **Description** | **Default value** | **Applies to** | | --- | --- | --- | --- | -| featuresRefreshRate | The SDK polls Split servers for changes to feature flags at this period (in seconds). | 30 seconds | In-memory. | -| segmentsRefreshRate | The SDK polls Split servers for changes to segments at this period (in seconds). | 30 seconds | In-memory. | -| impressionsRefreshRate | The treatment log captures which customer saw what treatment (on, off, and so on) at what time. This log is periodically flushed back to Split servers. This configuration controls how quickly the cache expires after a write (in seconds). | 300 seconds | In-memory. | -| metricsRefreshRate | The SDK caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers (in seconds). | 3600 seconds | In-memory. | +| featuresRefreshRate | The SDK polls Harness servers for changes to feature flags at this period (in seconds). | 30 seconds | In-memory. | +| segmentsRefreshRate | The SDK polls Harness servers for changes to segments at this period (in seconds). | 30 seconds | In-memory. | +| impressionsRefreshRate | The treatment log captures which customer saw what treatment (on, off, and so on) at what time. This log is periodically flushed back to Harness servers. This configuration controls how quickly the cache expires after a write (in seconds). | 300 seconds | In-memory. | +| metricsRefreshRate | The SDK caches diagnostic data that it periodically sends to Harness servers. This configuration controls how frequently this data is sent back to Harness servers (in seconds). | 3600 seconds | In-memory. | | eventsPushRate | How often the SDK sends events to the backend. | 10 seconds | In-memory. | -| labelsEnabled | Disable labels from being sent to Split backend. Labels may contain sensitive information. | true | All operation modes. | +| labelsEnabled | Disable labels from being sent to Harness servers. Labels may contain sensitive information. | true | All operation modes. | | connectionTimeout | HTTP client connection timeout (in ms). | 1500ms | In-memory. | -| apiKey | The Split SDK key. This entry is mandatory. If `localhost` is supplied as the SDK key, a localhost only client is created when `get_client` is called. | None | All operation modes. | +| apiKey | The SDK key. This entry is mandatory. If `localhost` is supplied as the SDK key, a localhost only client is created when `get_client` is called. | None | All operation modes. | | redisHost | The host that contains the Redis instance. | localhost | Redis-based storage setup. | | redisPort | The port of the Redis instance. | 6379 | Redis-based storage setup. | | redisDb | The db index on the Redis instance. | 0 | Redis-based storage setup. | @@ -882,8 +882,8 @@ The SDK has a number of knobs for configuring performance. Each knob is tuned to | eventsBulkSize | How many events to package when submiting them to the split servers | 5000 | In-memory. | | impressionsQueueSize | Max number of impressions to accumulate before sending them to the backend. | 10000 | In-memory. | | impressionsBulkSize | How many impressions to package when submiting them to the split servers | 5000 | In-memory. | -| IPAddressesEnabled | Flag to disable IP addresses and host name from being sent to the Split backend. | True | Redis, In-memory. | -| impressionsMode | This configuration defines how impressions are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Split; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. In DEBUG mode, ALL impressions are queued and sent to Split. Use DEBUG mode when you want every impression to be logged in Split's user interface when trying to debug your SDK setup. This setting does not impact the impression listener which will receives all generated impressions. | `'optimized'` | In-memory operation mode. | +| IPAddressesEnabled | Flag to disable IP addresses and host name from being sent to the Harness servers. | True | Redis, In-memory. | +| impressionsMode | This configuration defines how impressions are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Harness; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Harness FME and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. In DEBUG mode, ALL impressions are queued and sent to Harness. Use DEBUG mode when you want every impression to be logged in Harness FME when trying to debug your SDK setup. This setting does not impact the impression listener which will receives all generated impressions. | `'optimized'` | In-memory operation mode. | | streamingEnabled | Boolean flag to enable the streaming service as default synchronization mechanism. In the event of an issue with streaming, the SDK will fallback to the polling mechanism. If false, the SDK will poll for changes as usual without attempting to use streaming. | True | In-memory operation mode. | | flagSetsFilter | This setting allows the SDK to only synchronize the feature flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the SDK instance, bringing all the benefits from a reduced payload. | None | @@ -956,11 +956,11 @@ SPLITIO = { ## Localhost mode -A developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the Split SDK can be started in **localhost** mode (aka off-the-grid mode). In this mode, the SDK neither polls nor updates Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the features. To use the SDK in localhost mode, replace the SDK Key with "localhost", as shown in the example below: +A developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the SDK can be started in **localhost** mode (aka off-the-grid mode). In this mode, the SDK neither polls nor updates Harness servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the features. To use the SDK in localhost mode, replace the SDK Key with "localhost", as shown in the example below: With this mode, you can instantiate the SDKS using one of the following methods: -* JSON: Full support, for advanced cases or replicating an environment by pulling rules from the Split cloud (from version `9.4.0`). +* JSON: Full support, for advanced cases or replicating an environment by pulling rules from Harness FME servers (from version `9.4.0`). * YAML: Supports dynamic configs, individual targets, and default rules (from version `8.0.0`). * .split: Legacy option, only treatment result. @@ -1318,7 +1318,7 @@ new-navigation v3 ## Manager -Use the Split Manager to get a list of feature flags available to the split client. +Use the Split Manager to get a list of feature flags available to the SDK factory client. To instantiate a Manager in your code base, use the same factory that you used for your client. @@ -1390,7 +1390,7 @@ SplitView = namedtuple('SplitView', ['name', 'traffic_type', 'killed', 'treatmen ## Listener -Split SDKs send impression data back to Split servers periodically when evaluating feature flags. To send this information to a location of your choice, define and attach an *impression listener*. Use the SDK's `impressionListener` parameter, where you can add an implementation of `ImpressionListener`. This implementation **must** define the `log_impression` method. It receives data in the following schema. +FME SDKs send impression data back to Harness servers periodically when evaluating feature flags. To send this information to a location of your choice, define and attach an *impression listener*. Use the SDK's `impressionListener` parameter, where you can add an implementation of `ImpressionListener`. This implementation **must** define the `log_impression` method. It receives data in the following schema. | **Name** | **Type** | **Description** | | --- | --- | --- | diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/ruby-sdk.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/ruby-sdk.md index 663fa07258d..6eb22b2c499 100644 --- a/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/ruby-sdk.md +++ b/docs/feature-management-experimentation/20-sdks-and-infrastructure/server-side-sdks/ruby-sdk.md @@ -36,15 +36,15 @@ gem install splitclient-rb -v '~> 8.5.0' Since version 2.0.0 of the split-synchronizer, we use a more efficient scheme to store impressions in Redis. This approach is faster and easier on your Redis instances, since it yields better throughput of impressions to the backend. If you use this SDK with the Synchronizer in Redis or Proxy mode, you need the newest versions of our Split Synchronizer. We recommend that once you're using SDK versions compatible with Split-Sync 2.0 on all your applications pointing to the redis instance maintained by the Split-Sync, you disable backwards compatibility. This is as easy as changing a parameter to `true` on the JSON config or an environment variable to `on` if you're using the docker image. ::: -### 2. Instantiate the SDK and create a new Split client +### 2. Instantiate the SDK and create a new SDK factory client -When the SDK is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds, depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while its in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). +When the SDK is instantiated, it starts background tasks to update an in-memory cache with small amounts of data fetched from Harness servers. This process can take up to a few hundred milliseconds, depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific feature flag while its in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). -To make sure the SDK is properly loaded before asking it for a treatment, block it until the SDK is ready. You can do this by using the `block_until_ready` method of the Split Client (or Manager) as part of the instantiation process of the SDK as shown below. Do this as a part of the startup sequence of your application. +To make sure the SDK is properly loaded before asking it for a treatment, block it until the SDK is ready. You can do this by using the `block_until_ready` method of the SDK factory client (or Manager) as part of the instantiation process of the SDK as shown below. Do this as a part of the startup sequence of your application. -We recommend instantiating the Split factory once as a singleton and reusing it throughout your application. +We recommend instantiating the SDK factory once as a singleton and reusing it throughout your application. -Configure the SDK with the SDK key for the Split environment that you would like to access. The SDK key is available in the Split user interface, on your Admin settings page, API keys section. Select a server-side SDK API key. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. +Configure the SDK with the SDK key for the FME environment that you would like to access. The SDK key is available in Harness FME Admin settings. Select a server-side SDK API key. See [API keys](https://help.split.io/hc/en-us/articles/360019916211) to learn more. ```ruby title="Ruby" require 'splitclient-rb' @@ -68,7 +68,7 @@ split_factory = SplitIoClient::SplitFactory.new('YOUR_SDK_KEY') Rails.configuration.split_client = split_factory.client ``` -To access the SDK client in your controllers, use the code snippet below: +To access the SDK factory client in your controllers, use the code snippet below: ```ruby title="Ruby" Rails.application.config.split_client @@ -78,7 +78,7 @@ Now you can start asking the SDK to evaluate treatments for your customers. ### SDK Server Compatibility -The Split Ruby SDK has been tested as a standalone app using the following web servers: +The Ruby SDK has been tested as a standalone app using the following web servers: * Puma * Passenger * Unicorn @@ -89,7 +89,7 @@ For other setups, contact [support@split.io](mailto:support@split.io). **Note:** This is only applicable when using "memory storage". -During the start of your application, the SDK spawns multiple threads. Each thread has an infinite loop inside, which is used to fetch feature flags/segments or send impressions/events to the Split service continuously. When using Unicorn or Puma in cluster mode (i.e. with `workers` > 0) the application server will spawn multiple child processes, but they won't recreate the threads that existed in the parent process. So, if your application is running in Unicorn or Puma in cluster mode you need to make two small extra steps. +During the start of your application, the SDK spawns multiple threads. Each thread has an infinite loop inside, which is used to fetch feature flags/segments or send impressions/events to the FME service continuously. When using Unicorn or Puma in cluster mode (i.e. with `workers` > 0) the application server will spawn multiple child processes, but they won't recreate the threads that existed in the parent process. So, if your application is running in Unicorn or Puma in cluster mode you need to make two small extra steps. For both servers, you need to have the following line in your `config/initializers/splitclient.rb`: @@ -129,19 +129,19 @@ on_worker_boot do end ``` -By doing the above, the SDK recreates the threads for each new worker and prevents the master process (that doesn't handle requests) from needlessly querying the Split service. +By doing the above, the SDK recreates the threads for each new worker and prevents the master process (that doesn't handle requests) from needlessly querying the service. :::danger[Server spawning method] -If you are running NGINX with `thread_spawn_method = 'smart'`, use our Redis integration with the [Split Synchronizer](https://help.split.io/hc/en-us/articles/360019686092-Split-synchronizer) or contact [support@split.io](mailto:support@split.io) for alternatives to run Split. +If you are running NGINX with `thread_spawn_method = 'smart'`, use our Redis integration with the [Split Synchronizer](https://help.split.io/hc/en-us/articles/360019686092-Split-synchronizer) or contact [support@split.io](mailto:support@split.io) for alternatives to run FME. ::: ## Using the SDK ### Basic use -After you instantiate the SDK client, you can start using the `get_Treatment` method of the SDK client to decide what version of your features your customers are served. The method requires the `FEATURE_FLAG_NAME` attribute that you want to ask for a treatment and a unique `KEY` attribute that corresponds to the end user that you want to serve the feature to. +After you instantiate the SDK factory client, you can start using the `get_Treatment` method of the SDK factory client to decide what version of your features your customers are served. The method requires the `FEATURE_FLAG_NAME` attribute that you want to ask for a treatment and a unique `KEY` attribute that corresponds to the end user that you want to serve the feature to. -From there, you simply need to use an if-else-if block as shown below and insert the code for the different treatments that you defined in the Split user interface. Remember the final else branch in your code to handle the client returning [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). +From there, you simply need to use an if-else-if block as shown below and insert the code for the different treatments that you defined in Harness FME. Remember the final else branch in your code to handle the client returning [the control treatment](https://help.split.io/hc/en-us/articles/360020528072-Control-treatment). ```ruby title="Ruby" ## The key here represents the ID of the user, account, etc. you're trying to evaluate a treatment for @@ -160,7 +160,7 @@ end To [target based on custom attributes](https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes), the SDK's `get_treatment` method needs to be passed an attribute map at runtime. -In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `get_treatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in the Split user interface to decide whether to show the `on` or `off` treatment to this account. +In the example below, we are rolling out a feature flag to users. The provided attributes `plan_type`, `registered_date`, `permissions`, `paying_customer`, and `deal_size` are passed to the `get_treatment` call. These attributes are compared and evaluated against the attributes used in the rollout plan as defined in Harness FME to decide whether to show the `on` or `off` treatment to this account. The `get_treatment` method supports five types of attributes: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are: @@ -193,7 +193,7 @@ end ### Multiple evaluations at once -In some instances, you may want to evaluate treatments for multiple feature flag at once. Use the different variations of `get_treatments` method from the split client to do this. +In some instances, you may want to evaluate treatments for multiple feature flag at once. Use the different variations of `get_treatments` method from the SDK factory client to do this. * `get_treatments`': Pass a list of the feature flag names you want treatments for. * `get_treatments_by_flag_set`: Evaluate all flags that are part of the provided set name and are cached on the SDK instance. * `get_treatments_by_flag_sets`: Evaluate all flags that are part of the provided set names and are cached on the SDK instance. @@ -227,7 +227,7 @@ To [leverage dynamic configurations with your treatments](https://help.split.io/ This method will return an object containing the treatment and associated configuration. -The config element will be a stringified version of the configuration JSON defined in the Split user interface. If there are no configs defined for a treatment, the SDK returns `None` for the config parameter. +The config element will be a stringified version of the configuration JSON defined in Harness FME. If there are no configs defined for a treatment, the SDK returns `None` for the config parameter. This method takes the exact same set of arguments as the standard `get_treatment` method. See below for examples on proper usage: @@ -281,7 +281,7 @@ end ### Shutdown -Call the `.destroy` method before letting a process using the SDK exit, as this method gracefully shuts down the Split SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. +Call the `.destroy` method before letting a process using the SDK exit, as this method gracefully shuts down the SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions. ```ruby title="Ruby" client.destroy @@ -295,23 +295,23 @@ A call to the `destroy()` method also destroys the factory object. When creating ## Track -Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your feature flags on your users’ actions and metrics. +Use the `track` method to record any actions your customers perform. Each action is known as an `event` and corresponds to an `event type`. Calling `track` through one of our SDKs or via the API is the first step to and allows you to measure the impact of your feature flags on your users’ actions and metrics. [Learn more](https://help.split.io/hc/en-us/articles/360020585772) about using track events in feature flags. In the examples below, you can see that the `.track()` method can take up to four arguments. The proper data type and syntax for each are: * **key:** The `key` variable used in the `get_treatment` call and firing this track event. The expected data type is **String**. -* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in your instance of Split. +* **TRAFFIC_TYPE:** The traffic type of the key in the track call. The expected data type is **String**. You can only pass values that match the names of [traffic types](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) that you have defined in Harness FME. * **EVENT_TYPE:** The event type that this event should correspond to. The expected data type is **String**. Full requirements on this argument are: * Contains 63 characters or fewer. * Starts with a letter or number. * Contains only letters, numbers, hyphen, underscore, or period. * This is the regular expression we use to validate the value: `[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}` * **VALUE:** (Optional) The value used in creating the metric. This field can be sent in as nil or 0 if you intend to only use the count function when creating a metric. The expected data type is **Integer** or **Float**. -* **PROPERTIES:** (Optional) A Hash of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. Split currently supports three types of properties: strings, numbers, and booleans. +* **PROPERTIES:** (Optional) A Hash of key value pairs that can be used to filter your metrics. Learn more about event property capture in the [Events](https://help.split.io/hc/en-us/articles/360020585772-Events#event-properties) guide. FME currently supports three types of properties: strings, numbers, and booleans. -The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK successfully queued the event to be sent back to Split's servers on the next event post. The SDK returns `false` if the current queue size is equal to the config set by `events_queue_size` or if an incorrect input to the `track` method is provided. +The `track` method returns a boolean value of `true` or `false` to indicate whether or not the SDK successfully queued the event to be sent back to Harness servers on the next event post. The SDK returns `false` if the current queue size is equal to the config set by `events_queue_size` or if an incorrect input to the `track` method is provided. In the case that a bad input has been provided, refer to the [Events](https://help.split.io/hc/en-us/articles/360020585772-Track-events) guide for more information about our SDK's expected behavior. @@ -355,22 +355,22 @@ The SDK has a number of knobs for configuring performance. Each knob is tuned to | transport_debug_enabled | Super verbose mode that prints network payloads among others. | false | | connection_timeout| HTTP client connection timeout (in seconds). | 5s | | read_timeout | HTTP socket read timeout (in seconds). | 5s | -| features_refresh_rate |The SDK polls Split servers for changes to feature flags at this period (in seconds). | 5s | -| segments_refresh_rate | The SDK polls Split servers for changes to segments at this period (in seconds). | 60s | -| telemetry_refresh_rate | The SDK caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers (in seconds). | 3600s | +| features_refresh_rate |The SDK polls Harness servers for changes to feature flags at this period (in seconds). | 5s | +| segments_refresh_rate | The SDK polls Harness servers for changes to segments at this period (in seconds). | 60s | +| telemetry_refresh_rate | The SDK caches diagnostic data that it periodically sends to Harness servers. This configuration controls how frequently this data is sent back to Harness servers (in seconds). | 3600s | | impressions_refresh_rate | How often impressions are sent out (in seconds). | 60s | | events_push_rate | How often events are sent out (in seconds). | 60s | | cache_adapter| Where to store feature flags and impressions: `:memory` or `:redis` | `:memory` | | redis_url | Redis URL or hash with configuration for SDK to connect to. See [http://www.rubydoc.info/github/redis/redis-rb/Redis%3Ainitialize](http://www.rubydoc.info/github/redis/redis-rb/Redis%3Ainitialize) | 'redis://127.0.0.1:6379/0' | | mode | Whether the SDK is running in `standalone mode` using memory storage or `consumer mode` using an external storage. See Redis integration. | `:standalone` | | redis_namespace | Prefix to add to elements in Redis cache when having to share Redis with other applications. | `"SPLITIO/ruby-#{VERSION}"` | -| labels_enabled | Disable labels from being sent to the Split backend. Labels may contain sensitive information. | true | +| labels_enabled | Disable labels from being sent to the Harness servers. Labels may contain sensitive information. | true | | impressions_queue_size | The size of the impressions queue in case of `cache_adapter == :memory`. | 5000 | | events_queue_size | The size of the events queue in case of `cache_adapter == :memory`. | 500 | | impressions_bulk_size | Max number of impressions to be sent to the backend on each post. | impressions_queue_size | -| ip_addresses_enabled | Flag to disable IP addresses and host name from being sent to the Split backend. | true | +| ip_addresses_enabled | Flag to disable IP addresses and host name from being sent to the Harness servers. | true | | streaming_enabled | Boolean flag to enable the streaming service as default synchronization mechanism. In the event of an issue with streaming, the SDK will fallback to the polling mechanism. If false, the SDK will poll for changes as usual without attempting to use streaming. | true | -| impressions_mode | Defines how impressions are queued on the SDK. Supported modes are OPTIMIZED(`:optimized`), NONE(`:none`), and DEBUG(`:debug`). In OPTIMIZED mode, only unique impressions are queued and posted to Split; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Split and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, all impressions are queued and sent to Split; this is useful for validations. Use DEBUG mode when you want every impression to be logged in Split user inferface when trying to debug your SDK setup. This setting does not impact the impression listener which receives all generated impressions locally. | `:optimized` | +| impressions_mode | Defines how impressions are queued on the SDK. Supported modes are OPTIMIZED(`:optimized`), NONE(`:none`), and DEBUG(`:debug`). In OPTIMIZED mode, only unique impressions are queued and posted to Harness; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Harness FME and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, all impressions are queued and sent to Harness; this is useful for validations. Use DEBUG mode when you want every impression to be logged in Harness when trying to debug your SDK setup. This setting does not impact the impression listener which receives all generated impressions locally. | `:optimized` | | flag_sets_filter | This setting allows the SDK to only synchronize the feature flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the SDK instance, bringing all the benefits from a reduced payload. | nil | To set each of these parameters, use the syntax below: @@ -388,11 +388,11 @@ split_client = split_factory.client **Configuring this Redis integration section is optional for most setups. Read the information below to determine if it might be useful for your project.** -By default, the Split client stores the state it needs to compute treatments (rollout plans, segments, and so on) in memory. As a result, it is easy to get set up with Split by instantiating a client and starting to use it. Configuring this Redis integration section is optional for most setups. +By default, the SDK factory client stores the state it needs to compute treatments (rollout plans, segments, and so on) in memory. As a result, it is easy to get set up with FME by instantiating a client and starting to use it. Configuring this Redis integration section is optional for most setups. -This simplicity hides one important detail that is worth exploring. Because each Split client downloads and stores state separately, a change in a feature flag is picked up by every client on its own schedule. Thus, if a customer issues back-to-back requests that are served by two different machines behind a load balancer, the customer can see different treatments for the same feature flag because one Split client may not have picked up the latest change. This drift in clients is natural and usually ignorable as long as each client sets an aggressive value for `features_refresh_rate` and `segments_refresh_rate`. You can learn more about setting these rates in the [Configuration section](#configuration). +This simplicity hides one important detail that is worth exploring. Because each SDK factory client downloads and stores state separately, a change in a feature flag is picked up by every client on its own schedule. Thus, if a customer issues back-to-back requests that are served by two different machines behind a load balancer, the customer can see different treatments for the same feature flag because one SDK factory client may not have picked up the latest change. This drift in clients is natural and usually ignorable as long as each client sets an aggressive value for `features_refresh_rate` and `segments_refresh_rate`. You can learn more about setting these rates in the [Configuration section](#configuration). -However, if your application requires a total guarantee that Split clients across your entire infrastructure pick up a change in a feature flag at the exact same time, the only way to ensure that is to externalize the state of the Split client in a data store hosted on your infrastructure. +However, if your application requires a total guarantee that SDK clients across your entire infrastructure pick up a change in a feature flag at the exact same time, the only way to ensure that is to externalize the state of the SDK factory client in a data store hosted on your infrastructure. We currently support Redis for this external data store. @@ -406,7 +406,7 @@ Follow the steps in our [Split Synchronizer](https://help.split.io/hc/en-us/arti In consumer mode, a client can be embedded in your application code and respond to calls to `get_treatment` by retrieving state from the data store (Redis in this case). -Here is how to configure and get treatments for a Split client in consumer mode. +Here is how to configure and get treatments for a SDK factory client in consumer mode. ```ruby title="Ruby" options = { @@ -449,7 +449,7 @@ This functionality is currently not supported for this SDK, but is coming in a f ## Localhost mode -Features start their life on one developer's machine. A developer should be able to put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the Split SDK can be started in **localhost** mode (aka off-the-grid mode). In this mode, the SDK neither polls nor updates Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the features. +Features start their life on one developer's machine. A developer should be able to put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the SDK can be started in **localhost** mode (aka off-the-grid mode). In this mode, the SDK neither polls nor updates Harness servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the features. To use the SDK in localhost mode, replace the SDK Key with "localhost", as shown in the example below: @@ -493,7 +493,7 @@ factory = SplitIoClient::SplitFactoryBuilder.build('localhost', split_file: '/wh ## Manager -Use the Split Manager to get a list of feature flags available to the Split client. To instantiate a Manager in your code base, use the same factory that you used for your client. +Use the Split Manager to get a list of feature flags available to the SDK factory client. To instantiate a Manager in your code base, use the same factory that you used for your client. ```ruby title="Manager" ## Reusing the split_factory created originally. @@ -531,7 +531,7 @@ The `feature_flag` object referenced above has the following structure. ## Listener -Split SDKs send impression data back to Split servers periodically when evaluating feature flags. To send this information to a location of your choice, define and attach an *impression listener*. +FME SDKs send impression data back to Harness servers periodically when evaluating feature flags. To send this information to a location of your choice, define and attach an *impression listener*. The SDK sends the generated impressions to the impression listener right away. However, to avoid blocking the caller thread, use the second parameter to specify the size of the queue acting as a buffer. Refer to the followoing snippet: From 3b2b6a62be6c1d4adf9cb6482e49d1ad1a6ec20b Mon Sep 17 00:00:00 2001 From: lena sano Date: Sun, 9 Mar 2025 12:14:42 -0300 Subject: [PATCH 11/19] add FME landing page tile for SDK and Infra --- .../Docs/data/featureManagementExperimentationData.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/Docs/data/featureManagementExperimentationData.ts b/src/components/Docs/data/featureManagementExperimentationData.ts index 40752aff8cd..a2eda448d19 100644 --- a/src/components/Docs/data/featureManagementExperimentationData.ts +++ b/src/components/Docs/data/featureManagementExperimentationData.ts @@ -27,6 +27,13 @@ import { MODULES } from "@site/src/constants" "Platforms and technologies supported in FME", link: "/docs/feature-management-experimentation/getting-started/whats-supported", }, + { + title: "Development guides", + module: MODULES.fme, + description: + "Guides for using Harness FME with popular languages and platforms, including mobile development", + link: "/docs/feature-management-experimentation/sdks-and-infrastructure", + }, ], }, { From 26fb595cefb7b98d598c118351cd0bfb55ce4514 Mon Sep 17 00:00:00 2001 From: lena sano Date: Sun, 9 Mar 2025 22:00:15 -0300 Subject: [PATCH 12/19] rename files to kebab case --- ...dejs-sdk-error-node-modules-has-no-exported-member-splitio.md} | 0 ...-in-puma.md => ruby-sdk-close-wait-tcp-connections-in-puma.md} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/{nodejs-sdk-error-node_modules-has-no-exported-member-splitio.md => nodejs-sdk-error-node-modules-has-no-exported-member-splitio.md} (100%) rename docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/{ruby-sdk-close_wait-tcp-connections-in-puma.md => ruby-sdk-close-wait-tcp-connections-in-puma.md} (100%) diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-error-node_modules-has-no-exported-member-splitio.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-error-node-modules-has-no-exported-member-splitio.md similarity index 100% rename from docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-error-node_modules-has-no-exported-member-splitio.md rename to docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/nodejs-sdk-error-node-modules-has-no-exported-member-splitio.md diff --git a/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-close_wait-tcp-connections-in-puma.md b/docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-close-wait-tcp-connections-in-puma.md similarity index 100% rename from docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-close_wait-tcp-connections-in-puma.md rename to docs/feature-management-experimentation/20-sdks-and-infrastructure/faqs-server-side-sdks/ruby-sdk-close-wait-tcp-connections-in-puma.md From 356a2c20ed635df726e506096b7b794b7f38fef7 Mon Sep 17 00:00:00 2001 From: lena sano Date: Sun, 9 Mar 2025 22:01:07 -0300 Subject: [PATCH 13/19] fix SDK & Infra link on FME landing page --- .../Docs/data/featureManagementExperimentationData.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Docs/data/featureManagementExperimentationData.ts b/src/components/Docs/data/featureManagementExperimentationData.ts index a2eda448d19..9e698fa31bc 100644 --- a/src/components/Docs/data/featureManagementExperimentationData.ts +++ b/src/components/Docs/data/featureManagementExperimentationData.ts @@ -32,7 +32,7 @@ import { MODULES } from "@site/src/constants" module: MODULES.fme, description: "Guides for using Harness FME with popular languages and platforms, including mobile development", - link: "/docs/feature-management-experimentation/sdks-and-infrastructure", + link: "/docs/feature-management-experimentation/sdks-and-infrastructure/sdk-overview", }, ], }, From 246fe259c57f4c4e07affccd6166f64727cdc597 Mon Sep 17 00:00:00 2001 From: lena sano Date: Mon, 10 Mar 2025 10:47:40 -0300 Subject: [PATCH 14/19] update FME's structure image --- .../docs/FMEArchitectureObjectsImage.js | 36 + .../docs/key-concepts-updated.md | 124 + .../10-getting-started/docs/key-concepts.md | 2 +- .../static/fme-architecture-objects-dark.svg | 2 + .../static/fme-architecture-objects-light.svg | 2 + .../fme-architecture-objects.excalidraw | 5157 +++++++++++++++++ 6 files changed, 5322 insertions(+), 1 deletion(-) create mode 100644 docs/feature-management-experimentation/10-getting-started/docs/FMEArchitectureObjectsImage.js create mode 100644 docs/feature-management-experimentation/10-getting-started/docs/key-concepts-updated.md create mode 100644 docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects-dark.svg create mode 100644 docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects-light.svg create mode 100644 docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects.excalidraw diff --git a/docs/feature-management-experimentation/10-getting-started/docs/FMEArchitectureObjectsImage.js b/docs/feature-management-experimentation/10-getting-started/docs/FMEArchitectureObjectsImage.js new file mode 100644 index 00000000000..27b59b471e8 --- /dev/null +++ b/docs/feature-management-experimentation/10-getting-started/docs/FMEArchitectureObjectsImage.js @@ -0,0 +1,36 @@ +import { useColorMode } from '@docusaurus/theme-common'; + +import FmeLight from '@site/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects-light.svg'; +import FmeDark from '@site/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects-dark.svg' ; + +const LightVersion = () => { + return ; +}; + +const DarkVersion = () => { + return ; +}; + +const FMEArchitectureObjectsImage = () => { + const { isDarkTheme } = useColorMode(); + return isDarkTheme ? : ; +}; + +export default FMEArchitectureObjectsImage; + + + + + +/* +import useBaseUrl from '@docusaurus/useBaseUrl'; +import ThemedImage from '@theme/ThemedImage'; + + +*/ \ No newline at end of file diff --git a/docs/feature-management-experimentation/10-getting-started/docs/key-concepts-updated.md b/docs/feature-management-experimentation/10-getting-started/docs/key-concepts-updated.md new file mode 100644 index 00000000000..7be5a1a688e --- /dev/null +++ b/docs/feature-management-experimentation/10-getting-started/docs/key-concepts-updated.md @@ -0,0 +1,124 @@ +--- +title: Key concepts +sidebar_label: Key concepts (updated diagram) +sidebar_position: 3 +helpdocs_is_private: false +helpdocs_is_published: true +--- + +

+ +

+ +## Key concepts +Take 5 minutes to learn the foundational concepts of Harness Feature Management & Experimentation. + +## What is a feature flag? +A feature flag wraps or gates a section of your code, allowing it to be selectively turned on or off remotely with precision, down to the level of an individual user, at any time, without a new code deployment. + +### Decouple your deploy from your feature release +Feature flags allow you to decouple your deploy from your release, so your work in progress and new features are deployed in a turned-off state to any environment, which includes production, without impacting your users. + +### Control your release with targeting rules +Once your code is deployed, you can instantly turn on or off features for any individual user, group of users, or percentage of users, by creating or updating targeting rules. This approach facilitates faster software delivery practices with greater safety, including: + +* Trunk-based development to reduce time lost merging code branches +* Testing in production to allow dev, QA, and stakeholder review without impacting your users +* Early access or beta testing for a subset of your users in production +* Canary releases and monitored rollouts to limit the blast radius of release incidents +* Instant kill switches to shut off exposure to a feature without rollback or redeploy +* Infrastructure migration without downtime or risk of data loss +* Experimentation and A/B testing to make bigger bets with less risk + +## The role of data in Harness FME +FME provides visibility into your controlled releases by comparing data about feature flag evaluations with data about what happened after those evaluations. The data points that feed those comparisons are impressions and events. The results of those comparisons are called metrics. + +### Impressions +An impression is a record of a targeting decision made. It is created automatically each time a feature flag is evaluated and contains details about the user or unique key for which the evaluation was performed, the targeting decision, the targeting rule that drove that decision, and a time stamp. Refer to the [Impressions](https://help.split.io/hc/en-us/articles/360020585192-Impressions) guide for more information. + +### Events +An event is a record of user or system behavior. Events can be as simple as a page visited, a button clicked, or response time observed, and as complex as a transaction record with a detailed list of properties. An event doesn’t refer to a feature flag. The association between flag evaluations and events is computed for you. An event, associated with a user (or other unique keys), arriving after a flag decision for that same unique key, is attributed to that evaluation by FME’s attribution engine. + +To be ingested by FME, an event must contain the same user or unique key for which a feature flag evaluation was performed and a time stamp. Events are sent to FME from within your application, either from an existing customer data platform or error subsystem, or with a bulk upload using [Split Admin API](https://docs.split.io). Numerous events in integrations streamline event ingest for you. + +### Metrics +FME calculates metrics by attributing events to impressions and applying metric definitions to them. A metric definition can be as simple as a count of events per user or as complex as an average of values pulled from an event’s property after filtering those same events by another property. + +For example, from a stream of room_reservation events, calculate the average number of room nights booked for platinum members by examining the room_nights property after filtering the room_reservation events to those where the property club_membership = platinum. + +To promote one version of the truth, metrics are defined in a central location, not on a flag-by-flag basis, and all metrics are calculated for all flags. FME lets you elevate any metric your account created to be a key metric for a given feature flag. Then all the remaining metrics are sorted by impact and displayed immediately below the key metrics. This design, unique to FME, avoids blind spots caused by only looking for what you expect to find which automatically surfaces unexpected impacts. Refer to the [Metrics](https://help.split.io/hc/en-us/articles/22005565241101-Metrics) guide for more information. + +### Alerts +Alerts notify metric stakeholders and the team rolling out a particular feature when a metric threshold has exceeded a rollout or experiment that uses a percentage rollout rule. + +Alerts, like the metrics they are based on, are centrally defined once, and then applied to every rollout or experiment automatically. This is another design unique to FME. Our goal is to make learning and safety at speed the default experience, for every rollout. Once you define thresholds for metrics, any future rollout or experiment that exceeds them will fire an alert. When that happens, notifications are sent out, and an alert box is presented on the Targeting and Alerts tabs for the feature flag in question. Refer to the [Configuring metric alerting](https://help.split.io/hc/en-us/articles/19832312225293-Configuring-metric-alerting) guide for more information. + +## Using FME in your application +Targeting decisions are made locally, in memory, from within your own application code. There is never a reason to send private user data to Harness. Let’s take a look at how this is accomplished. + +### FME SDKs +To use Harness FME, include and initialize one of FME SDKs in your application. Once the SDK is initialized, targeting rules are retrieved from a nearby content delivery network (CDN) node, cached inside your code, and updated in real-time in milliseconds using a streaming architecture. + +As needed, your application makes a just-in-time call to the FME SDK in local memory, passing the feature flag name, the userId or unique key, and optionally, a map of user or session attributes. The response is returned instantly, with no need for a network call. After the evaluation is performed, the SDK asynchronously returns an impression record to Harness. Refer to our [SDK overview](https://help.split.io/hc/en-us/articles/360033557092-SDK-overview) for more information. + +### Split Evaluator +As an alternative to using FME SDKs, you can make REST API calls to a Split Evaluator hosted inside your own infrastructure. Like the SDK, this method never requires you to send private user data to the Harness network. The evaluator makes it possible to operate from within languages that do not yet have a published FME SDK and should only be used in that case. Refer to the [Split Evaluator](https://help.split.io/hc/en-us/articles/360020037072-Split-Evaluator) guide for more information. + +## FME's structure +Harness FME is architected to support teams and organizations of any size, from a single developer to multiple value-stream enterprises. Take a moment to familiarize yourself with the concepts of your Harness account, project, environment, and objects, e.g., users, user groups, tags, traffic types, feature flags, segments, and metrics. + +import FMEArchitectureObjectsImage from '@site/docs/feature-management-experimentation/10-getting-started/docs/FMEArchitectureObjectsImage.js'; + + + +:::info[Note: Split Legacy settings locations] +Post migration to app.harness.io, Split legacy Project permissions, Change permissions and Data export permissions (marked in purple above) will move out of their current locations and into Harness RBAC management. + +New Admin API Key creation and management will move to Harness Service Accounts. Existing Split legacy Admin API Keys will continue to operate until revoked in the Split legacy location. +::: + +### Account +Your company has one Harness account. Your account is the highest level container. Harness FME support may ask you for your account ID to speed troubleshooting. You’ll find your account ID in the URL for every page you visit in Harness. + +### Users +A Harness user is someone with access to the Harness user interface. Administrators can invite new users to Harness. All paid plans include SSO for user authentication and can support either invites or just in time provisioning. + +### User Groups +A group is a convenient way to manage a collection of users in your account. You can use groups to grant administrative controls and grant environment, feature flag, or segment-level controls. Refer to the [Manage user groups](https://help.split.io/hc/en-us/articles/360020812952-Manage-user-groups) guide for more information. + +### Projects +Projects provide separation or partitioning of work to reduce clutter or to enforce security. All accounts have at least one project. Use multiple projects only when you want to deliberately separate the work of different teams, product lines, or areas of work from each other. By design, objects within FME are not meant to be shared or moved across projects. Refer to the [Projects](https://help.split.io/hc/en-us/articles/360023534451-Workspaces) guide for more information. + +### Environment +Within each project, you may have multiple environments, such as development, staging, and production. Refer to the [Environments](https://help.split.io/hc/en-us/articles/360019915771-Environments) guide for more information. + +### Feature Flags +Feature flags are created at the project level where you specify the feature flag name, traffic type, owners, and description. Targeting rules are then created and managed at the environment level as part of the feature flag definition. Refer to the [Feature flag management](https://help.split.io/hc/en-us/articles/9650375859597-Feature-flag-management) guide for more information. + +### Targeting rule +Targeting rules for each feature flag are created at the environment level. For example, this supports one set of rules in your staging environment and another in production. Rules may be based on user or device attributes, membership in a segment, a percentage of a randomly distributed population, a list of individually specified user or unique key targets, or any combination of the above. + + + +### Segment +A segment is a list of users or unique keys for targeting purposes. Segments are created at the environment level. Refer to the [Segments](https://help.split.io/hc/en-us/articles/360020407512-Create-a-segment) guide for more information. + +### Traffic type +Targeting decisions are made on a per-user or per unique key basis, but what are the available types of unique keys you intend to target? These are your traffic types, and you can define up to ten unique key types at the project level. + +For feature flags that make decisions or observe metrics at the userId level, the traffic type should be user. If decisions and observations are based on account membership (to facilitate all users for a particular customer being treated the same, for instance), the traffic type should be account. Other common types are anonymous and device, but you have total flexibility in employing different traffic types. Refer to the [Traffic type](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) guide for more information. + +### Tag +Use tags to organize and filter feature flags, segments, and metrics across the Harness user interface. Because they allow you to filter items in lists, they are a great way to filter by team, epic, layer of system (front-end vs back-end), or any other. Refer to the [Tags](https://help.split.io/hc/en-us/articles/360020839151-Tags) guide for more information on how to use them. + +### Statuses +Statuses provide a way for teams to indicate which stage of a release or rollout a feature is in at any given moment, and as a way for teammates to filter their feature flags to see only features in a particular stage of the internal release process. There is a fixed list of status types. Refer to the [Use statuses](https://help.split.io/hc/en-us/articles/4405023981197-Use-statuses) guide for more information. + + \ No newline at end of file diff --git a/docs/feature-management-experimentation/10-getting-started/docs/key-concepts.md b/docs/feature-management-experimentation/10-getting-started/docs/key-concepts.md index 66ea100cfaa..550573b9284 100644 --- a/docs/feature-management-experimentation/10-getting-started/docs/key-concepts.md +++ b/docs/feature-management-experimentation/10-getting-started/docs/key-concepts.md @@ -1,6 +1,6 @@ --- title: Key concepts -sidebar_label: Key concepts +sidebar_label: Key concepts (copy & paste) sidebar_position: 3 helpdocs_is_private: false helpdocs_is_published: true diff --git a/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects-dark.svg b/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects-dark.svg new file mode 100644 index 00000000000..43beabcda10 --- /dev/null +++ b/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects-dark.svg @@ -0,0 +1,2 @@ +Account(Account ID, Account name, Plan type) Admin API Key(Split Legacy)(if globally scoped)(used with SplitPublic API)User(a Harness login)User Group(a list of Harnesslogins)Tag(created in the Tag inputfield of a Segment, Featureflag, or Metric)Project(no FME data or FME relationships are shared between projects)Project permissions (Split Legacy) Anyone can access Restrict who can accessRequire comments Require title and comments forfeature flag, segment,environment, and metric changes.Traffic type(classifies the type ofkey, or uniqueidentifier, for whichfeature flag targetingdecisions are madeand data is collected)(control access rights/view permissions)Flag set(created using SplitPublic API)(a list of Featureflags)(optimizes SDKinitialization bylimiting the numberof downloadedFeature flagdefinitions to specificFlag set(s))Attribute /Traffic typeattribute(called Traffic typeAttributes in FMEAdmin settings)(can be used inFeature flagattribute basedtargeting rules)(called Metricdimension in FMEAdmin Settings)(appears underDimensionalAnalysis on MetricDetails page)(sent from SDK orintegration as eventproperty)Metric dimension/ Event propertyFeature flag metadata(Name, Traffic type, Owners,Tags, Description)Segment metadata(Name, Traffic type,Owners, Tags, Description)Metric metadata(Name, Traffic type,Owners, Tags,Description)Metric definitionAlert policy(Select desired impact[increase|decrease],Measure as [metrictype], Filter by, Cap at)Environment(allows separate control and data collection for staging, production, etc.)Change permissions Approval requiredData export permissions (Split Legacy) Any login user RestrictedEnvironment type Production Pre-productionAdmin API Key(Split Legacy)(if scoped toenvironment)(used with SplitPublic API)SDK API Key(always scoped toone environment)Feature flag definition(Treatments [variations],Targeting rules, Status,Editing permissionoverrides, Key metrics,Supporting metrics)Segment definition(list of keys, or identifiersfor end users of your app,Editing permissionoverrides)per Traffic type and Environment data pipelineData collectedImpressionsEvents(logs of rule evaluations, sent from SDKor Split Evaluator)(performance and behavioral data, sentfrom SDK, API, or integrations)AttributionMetrics impact calculationsAlert notifications(associates events toFeature flag treatments[variations])Experiment(Name, Owners, Tags,Hypothesis, Assignmentsource [Feature flag,Environment],Experiment scope [Start,End, Targeting rule,Baseline treatment,Comparison treatments]) Any login user (Split Legacy) Restricted (Split Legacy)(always scoped toone environment)(always scoped toone environment) \ No newline at end of file diff --git a/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects-light.svg b/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects-light.svg new file mode 100644 index 00000000000..59d91ec16b3 --- /dev/null +++ b/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects-light.svg @@ -0,0 +1,2 @@ +Account(Account ID, Account name, Plan type) Admin API Key(Split Legacy)(if globally scoped)(used with SplitPublic API)User(a Harness login)User Group(a list of Harnesslogins)Tag(created in the Tag inputfield of a Segment, Featureflag, or Metric)Project(no FME data or FME relationships are shared between projects)Project permissions (Split Legacy) Anyone can access Restrict who can accessRequire comments Require title and comments forfeature flag, segment,environment, and metric changes.Traffic type(classifies the type ofkey, or uniqueidentifier, for whichfeature flag targetingdecisions are madeand data is collected)(control access rights/view permissions)Flag set(created using SplitPublic API)(a list of Featureflags)(optimizes SDKinitialization bylimiting the numberof downloadedFeature flagdefinitions to specificFlag set(s))Attribute /Traffic typeattribute(called Traffic typeAttributes in FMEAdmin settings)(can be used inFeature flagattribute basedtargeting rules)(called Metricdimension in FMEAdmin Settings)(appears underDimensionalAnalysis on MetricDetails page)(sent from SDK orintegration as eventproperty)Metric dimension/ Event propertyFeature flag metadata(Name, Traffic type, Owners,Tags, Description)Segment metadata(Name, Traffic type,Owners, Tags, Description)Metric metadata(Name, Traffic type,Owners, Tags,Description)Metric definitionAlert policy(Select desired impact[increase|decrease],Measure as [metrictype], Filter by, Cap at)Environment(allows separate control and data collection for staging, production, etc.)Change permissions Approval requiredData export permissions (Split Legacy) Any login user RestrictedEnvironment type Production Pre-productionAdmin API Key(Split Legacy)(if scoped toenvironment)(used with SplitPublic API)SDK API Key(always scoped toone environment)Feature flag definition(Treatments [variations],Targeting rules, Status,Editing permissionoverrides, Key metrics,Supporting metrics)Segment definition(list of keys, or identifiersfor end users of your app,Editing permissionoverrides)per Traffic type and Environment data pipelineData collectedImpressionsEvents(logs of rule evaluations, sent from SDKor Split Evaluator)(performance and behavioral data, sentfrom SDK, API, or integrations)AttributionMetrics impact calculationsAlert notifications(associates events toFeature flag treatments[variations])Experiment(Name, Owners, Tags,Hypothesis, Assignmentsource [Feature flag,Environment],Experiment scope [Start,End, Targeting rule,Baseline treatment,Comparison treatments]) Any login user (Split Legacy) Restricted (Split Legacy)(always scoped toone environment)(always scoped toone environment) \ No newline at end of file diff --git a/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects.excalidraw b/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects.excalidraw new file mode 100644 index 00000000000..a2789b62604 --- /dev/null +++ b/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects.excalidraw @@ -0,0 +1,5157 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "b95IqzLyQgXfY8dSlQWtt", + "type": "rectangle", + "x": 1000.6032511490832, + "y": -97.11893864754273, + "width": 211.6835785563352, + "height": 96.69465455649623, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "Zv", + "roundness": null, + "seed": 230506694, + "version": 1359, + "versionNonce": 444238426, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605635, + "link": null, + "locked": false + }, + { + "id": "gMaAe28TK2nCyJo33jd4w", + "type": "rectangle", + "x": 992.9141624387444, + "y": -108.05100029235527, + "width": 212.57916006404886, + "height": 100.79169218495056, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "Zw", + "roundness": null, + "seed": 1327839238, + "version": 1211, + "versionNonce": 304885530, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605635, + "link": null, + "locked": false + }, + { + "id": "xH9CEBNrqK-qwZwg_Gi0S", + "type": "rectangle", + "x": 798.7214690434944, + "y": -99.40674348228441, + "width": 170.07421875, + "height": 78.72524699312714, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "Zx", + "roundness": null, + "seed": 1728857926, + "version": 693, + "versionNonce": 365640666, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605635, + "link": null, + "locked": false + }, + { + "id": "KU062cXceYRv1EjZQ9T-S", + "type": "rectangle", + "x": 792.8816252934944, + "y": -106.03174348228441, + "width": 170.07421875, + "height": 79.39642396907215, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "Zy", + "roundness": null, + "seed": 1708093062, + "version": 573, + "versionNonce": 325759130, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605635, + "link": null, + "locked": false + }, + { + "id": "mOAcPNyl5IJ6QuwMwcBWV", + "type": "rectangle", + "x": 599.3796721684944, + "y": -100.04346223228441, + "width": 170.07421875, + "height": 78.71987757731961, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "Zz", + "roundness": null, + "seed": 1210822086, + "version": 529, + "versionNonce": 1052609882, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605635, + "link": null, + "locked": false + }, + { + "id": "0dK5_bbPGmWBepJWRhdDa", + "type": "rectangle", + "x": 372.2499460312574, + "y": -167.65815216669864, + "width": 1251.1737274992058, + "height": 1769.3057042153353, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a0", + "roundness": null, + "seed": 1799226630, + "version": 1223, + "versionNonce": 1155691735, + "isDeleted": false, + "boundElements": [], + "updated": 1739350312306, + "link": null, + "locked": false + }, + { + "id": "xlM3UMfMi_QUGK53QYR1F", + "type": "text", + "x": 392.45307421248094, + "y": -159.57489625682268, + "width": 74.63993138074875, + "height": 27, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a1", + "roundness": null, + "seed": 8614982, + "version": 364, + "versionNonce": 292126426, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605635, + "link": null, + "locked": false, + "text": "Account", + "fontSize": 20, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Account", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "R3J01rThnB-JpikTr1SuH", + "type": "line", + "x": 372.46869921248094, + "y": -130.31497346714605, + "width": 1247.4849872350512, + "height": 1.3955951694165663, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a2", + "roundness": null, + "seed": 30997382, + "version": 704, + "versionNonce": 675845018, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605635, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 1247.4849872350512, + -1.3955951694165663 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "_kYRl3DxAM9gfzUHNW7YD", + "type": "text", + "x": 487.18354296248094, + "y": -157.76158066816686, + "width": 305.42412186707264, + "height": 23.45676345102084, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a4", + "roundness": null, + "seed": 628857542, + "version": 330, + "versionNonce": 1676087386, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605635, + "link": null, + "locked": false, + "text": "(Account ID, Account name, Plan type) ", + "fontSize": 17.3753803340895, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(Account ID, Account name, Plan type) ", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "xoMCQ8pODUS2Wia7owFz3", + "type": "rectangle", + "x": 403.68182587657776, + "y": -112.50634902594697, + "width": 162.296875, + "height": 131.82655575406278, + "angle": 0, + "strokeColor": "#6741d9", + "backgroundColor": "#d0bfff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a5", + "roundness": null, + "seed": 1376276998, + "version": 493, + "versionNonce": 1209529626, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605635, + "link": null, + "locked": false + }, + { + "id": "p2FqHowmmzI8t4WWyXugN", + "type": "text", + "x": 411.2924879026797, + "y": -105.6246418730808, + "width": 112.3111200928688, + "height": 44.497041023426455, + "angle": 0, + "strokeColor": "#6741d9", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a7", + "roundness": null, + "seed": 310124870, + "version": 637, + "versionNonce": 1716404698, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605635, + "link": null, + "locked": false, + "text": "Admin API Key\n(Split Legacy)", + "fontSize": 16.48038556423202, + "fontFamily": 6, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Admin API Key\n(Split Legacy)", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "-Ecn9fJuUmTKUpNycsLLM", + "type": "text", + "x": 412.4334350132558, + "y": -47.19686496324641, + "width": 138.7762093246117, + "height": 58.03756009727796, + "angle": 0, + "strokeColor": "#6741d9", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a8", + "roundness": null, + "seed": 171182214, + "version": 797, + "versionNonce": 698152602, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605635, + "link": null, + "locked": false, + "text": "(if globally scoped)\n(used with Split\nPublic API)", + "fontSize": 14.330261752414312, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(if globally scoped)\n(used with Split Public API)", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "XDWPThtj9vlkEajmz5X1J", + "type": "rectangle", + "x": 592.5398284184944, + "y": -106.66846223228441, + "width": 170.07421875, + "height": 79.203125, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aJ", + "roundness": null, + "seed": 1760364486, + "version": 393, + "versionNonce": 582423386, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605635, + "link": null, + "locked": false + }, + { + "id": "IGm13Efsrf7kpAw8LhlBk", + "type": "rectangle", + "x": 586.4206877934944, + "y": -112.64502473228441, + "width": 170.07421875, + "height": 79.203125, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aK", + "roundness": null, + "seed": 1589911302, + "version": 313, + "versionNonce": 1408210970, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605635, + "link": null, + "locked": false + }, + { + "id": "ABQFDtezw6MpmmSmcmqdB", + "type": "text", + "x": 592.6199065434944, + "y": -107.81510377196105, + "width": 35.023988008499146, + "height": 21.6, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aL", + "roundness": null, + "seed": 271994438, + "version": 273, + "versionNonce": 2100921562, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605635, + "link": null, + "locked": false, + "text": "User", + "fontSize": 16, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "User", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "Qena8B-fAEVR-6F_KM4Lw", + "type": "text", + "x": 592.1199065434944, + "y": -81.99707962167706, + "width": 157.79106140136724, + "height": 21.6, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aM", + "roundness": null, + "seed": 169907590, + "version": 496, + "versionNonce": 797603863, + "isDeleted": false, + "boundElements": [], + "updated": 1739348698630, + "link": null, + "locked": false, + "text": "(a Harness login)", + "fontSize": 16, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(a Harness login)", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "Gz4WPp8nEQDLdkBT8bN14", + "type": "rectangle", + "x": 787.6609221684944, + "y": -112.22705598228441, + "width": 170.07421875, + "height": 79.203125, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aN", + "roundness": null, + "seed": 901772486, + "version": 462, + "versionNonce": 871208538, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605635, + "link": null, + "locked": false + }, + { + "id": "9v36GokxB2GhdNb-XYTp8", + "type": "text", + "x": 795.8601409184944, + "y": -107.81510377196105, + "width": 45.02396750450134, + "height": 21.6, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aO", + "roundness": null, + "seed": 1286695942, + "version": 381, + "versionNonce": 559790874, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605635, + "link": null, + "locked": false, + "text": "Group", + "fontSize": 16, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Group", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "QoF1Dg7M_vZ4LgMMreFVN", + "type": "text", + "x": 794.3601409184944, + "y": -82.85986848228441, + "width": 157.79106140136724, + "height": 43.2, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aP", + "roundness": null, + "seed": 1290926918, + "version": 692, + "versionNonce": 302629401, + "isDeleted": false, + "boundElements": [], + "updated": 1739348706708, + "link": null, + "locked": false, + "text": "(a list of Harness\nlogins)", + "fontSize": 16, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(a list of Harness logins)", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "E9STsbRsWBrWDGCLe-e4r", + "type": "rectangle", + "x": 986.6621210005496, + "y": -113.45214061006664, + "width": 212.57916006404886, + "height": 99.26054106780947, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aQ", + "roundness": null, + "seed": 1826529926, + "version": 1316, + "versionNonce": 1420377242, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605635, + "link": null, + "locked": false + }, + { + "id": "vxZQZ7XkTotqhzNOw2WqW", + "type": "text", + "x": 1000.4943167797869, + "y": -107.81510377196105, + "width": 26.447982788085938, + "height": 21.6, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aR", + "roundness": null, + "seed": 255091142, + "version": 895, + "versionNonce": 1883937114, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605635, + "link": null, + "locked": false, + "text": "Tag", + "fontSize": 16, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Tag", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "ZpLWgaSf5vCv9LsXz970q", + "type": "text", + "x": 998.964638791756, + "y": -78.90976257955347, + "width": 190.48186411478315, + "height": 59.65359937595531, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ay", + "roundness": null, + "seed": 923810054, + "version": 1708, + "versionNonce": 1898077722, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false, + "text": "(created in the Tag input\nfield of a Segment, Feature\nflag, or Metric)", + "fontSize": 14.729283796532176, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(created in the Tag input field of a Segment, Feature flag, or Metric)", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "8CuK29XwH9xySY3TRkuDz", + "type": "rectangle", + "x": 430.33927626587206, + "y": 54.879707940189526, + "width": 1168.162706696684, + "height": 1496.0284650073986, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b4Y", + "roundness": null, + "seed": 2079270982, + "version": 2286, + "versionNonce": 1665111545, + "isDeleted": false, + "boundElements": [], + "updated": 1739350305607, + "link": null, + "locked": false + }, + { + "id": "T5R725zpo_ATPuQtPDr9x", + "type": "rectangle", + "x": 422.2470344115004, + "y": 47.68236794405805, + "width": 1169.408049750158, + "height": 1496.0284650073986, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b4Z", + "roundness": null, + "seed": 2108567430, + "version": 1995, + "versionNonce": 581411351, + "isDeleted": false, + "boundElements": [], + "updated": 1739350305607, + "link": null, + "locked": false + }, + { + "id": "c-rIipPmkQidKpM37P8jw", + "type": "rectangle", + "x": 411.0751850360199, + "y": 41.19901273819499, + "width": 1174.5274637028388, + "height": 1496.0284650073986, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b4a", + "roundness": null, + "seed": 1485459142, + "version": 1879, + "versionNonce": 1991012057, + "isDeleted": false, + "boundElements": [], + "updated": 1739350305607, + "link": null, + "locked": false + }, + { + "id": "JoXqhfeWtBPyrQYKmPaq2", + "type": "line", + "x": 412.115891454391, + "y": 80.34097654429596, + "width": 1170.5831852277784, + "height": 4.721181855404907, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b4b", + "roundness": null, + "seed": 421561862, + "version": 1474, + "versionNonce": 707995930, + "isDeleted": false, + "boundElements": [], + "updated": 1739279731181, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 1170.5831852277784, + -4.721181855404907 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "scMeP9YaO0BU05VGvl05I", + "type": "text", + "x": 435.1411218358378, + "y": 50.71387731396945, + "width": 64.7199547290802, + "height": 27, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b4c", + "roundness": null, + "seed": 812504390, + "version": 652, + "versionNonce": 2029535898, + "isDeleted": false, + "boundElements": [], + "updated": 1739279756130, + "link": null, + "locked": false, + "text": "Project", + "fontSize": 20, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Project", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "E5eN-YCYLWSZTVv8VBJUR", + "type": "text", + "x": 517.8715905858378, + "y": 51.76748446691825, + "width": 507.0391550569012, + "height": 23.66640307490961, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b4d", + "roundness": null, + "seed": 1741634694, + "version": 895, + "versionNonce": 1670794246, + "isDeleted": false, + "boundElements": [], + "updated": 1739279756130, + "link": null, + "locked": false, + "text": "(no FME data or FME relationships are shared between projects)", + "fontSize": 17.53066894437748, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(no FME data or FME relationships are shared between projects)", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "t17mjuarR6oKhNxpo45qA", + "type": "text", + "x": 439.33040381674573, + "y": 95.83037458314652, + "width": 261.25238037109375, + "height": 92.4173867240141, + "angle": 0, + "strokeColor": "#6741d9", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b4f", + "roundness": null, + "seed": 1426176966, + "version": 581, + "versionNonce": 1836588890, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false, + "text": "Project permissions (Split Legacy)\n\n Anyone can access\n Restrict who can access", + "fontSize": 17.114330874817423, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Project permissions (Split Legacy)\n\n Anyone can access\n Restrict who can access", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "xHf1slebHVhClVUC3CeCK", + "type": "ellipse", + "x": 440.47263611310836, + "y": 150.04045540333937, + "width": 10.066137933221853, + "height": 10.429207181753382, + "angle": 0, + "strokeColor": "#6741d9", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b4g", + "roundness": null, + "seed": 1310872326, + "version": 375, + "versionNonce": 984556570, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false + }, + { + "id": "-6QhR85PIizx_gqNPnEXI", + "type": "ellipse", + "x": 440.47263611310836, + "y": 171.19484118971673, + "width": 10.066137933221853, + "height": 10.429207181753382, + "angle": 0, + "strokeColor": "#6741d9", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b4h", + "roundness": null, + "seed": 1571684934, + "version": 512, + "versionNonce": 2082967770, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false + }, + { + "id": "En4R4o-R6cLRQVnhHOVko", + "type": "text", + "x": 806.6582358218378, + "y": 93.94619104783749, + "width": 263.0708640501642, + "height": 93.40574462122218, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b4i", + "roundness": null, + "seed": 914475398, + "version": 907, + "versionNonce": 1514571319, + "isDeleted": false, + "boundElements": [], + "updated": 1739349376712, + "link": null, + "locked": false, + "text": "Require comments\n Require title and comments for\nfeature flag, segment,\nenvironment, and metric changes.", + "fontSize": 17.297360115041144, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Require comments\n Require title and comments for feature flag, segment, environment, and metric changes.", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "grYyAm8TVgTYLUvQiiSNd", + "type": "rectangle", + "x": 808.2967108959774, + "y": 124.23272966354469, + "width": 9.942093164058178, + "height": 10.321903464707447, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b4j", + "roundness": null, + "seed": 668641478, + "version": 457, + "versionNonce": 938826937, + "isDeleted": false, + "boundElements": [], + "updated": 1739349376712, + "link": null, + "locked": false + }, + { + "id": "Pp6C5wxq1o4xuDvLy8j8z", + "type": "rectangle", + "x": 445.9599313777963, + "y": 232.0145949024498, + "width": 173.06405408619926, + "height": 271.45320003047925, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b4k", + "roundness": null, + "seed": 290824198, + "version": 1370, + "versionNonce": 1442268954, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false + }, + { + "id": "TAINn6-B8hrMyHnpiSBFx", + "type": "rectangle", + "x": 440.42567202113344, + "y": 222.001086688322, + "width": 172.52889425152793, + "height": 273.80881203393847, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b4l", + "roundness": null, + "seed": 1458548550, + "version": 1614, + "versionNonce": 1618170842, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false + }, + { + "id": "8GQXv8sGWiTtQXPN8iatp", + "type": "rectangle", + "x": 434.3558524089836, + "y": 212.2069728625334, + "width": 171.79582885120172, + "height": 277.509116212592, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b4m", + "roundness": null, + "seed": 1426445958, + "version": 1688, + "versionNonce": 22005914, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false + }, + { + "id": "e_3Lre4R2Uu0kljMWZcXo", + "type": "rectangle", + "x": 443.23584961810633, + "y": 273.5102097644474, + "width": 30.918150937311786, + "height": 15.959947900217863, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b4n", + "roundness": null, + "seed": 1414862278, + "version": 1055, + "versionNonce": 1614783834, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false + }, + { + "id": "6ljCoyQ1AonpeFrB1pSIs", + "type": "text", + "x": 448.1320109691659, + "y": 219.7824523036154, + "width": 88.72347546495344, + "height": 23.746389820243895, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b4o", + "roundness": null, + "seed": 2113563910, + "version": 835, + "versionNonce": 915794458, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false, + "text": "Traffic type", + "fontSize": 17.58991838536584, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Traffic type", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "MQgHecYqCDZbNLkr5QY9I", + "type": "text", + "x": 446.42216320515683, + "y": 250.45954468496075, + "width": 146.60213740632133, + "height": 120.41009699231677, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b4p", + "roundness": null, + "seed": 1332927558, + "version": 2047, + "versionNonce": 457545434, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false, + "text": "(classifies the type of\nkey, or unique\nidentifier, for which\nfeature flag targeting\ndecisions are made\nand data is collected)", + "fontSize": 14.865444073125525, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(classifies the type of key, or unique identifier, for which feature flag targeting decisions are made and data is collected)", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "_-mKK_kvDGLnp79fFcJ2s", + "type": "text", + "x": 437.4380991881825, + "y": 119.84549410230593, + "width": 356.41863019668875, + "height": 22.113196391754705, + "angle": 0, + "strokeColor": "#6741d9", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b4q", + "roundness": null, + "seed": 245064582, + "version": 1336, + "versionNonce": 1323412378, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false, + "text": "(control access rights/view permissions)", + "fontSize": 16.380145475373855, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(control access rights/view permissions)", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "cVkmxOd3A3UzBC0HbLrIZ", + "type": "rectangle", + "x": 640.6526439191158, + "y": 332.8319617649543, + "width": 135.51067525412682, + "height": 257.6615904874118, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b4r", + "roundness": null, + "seed": 1035010758, + "version": 2946, + "versionNonce": 127739994, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false + }, + { + "id": "BbZpz_ck5HTldoFIh2imo", + "type": "rectangle", + "x": 796.1268426234526, + "y": 333.1388289799987, + "width": 139.62106101559505, + "height": 259.1700261697964, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b4t", + "roundness": null, + "seed": 256948742, + "version": 2673, + "versionNonce": 1028328730, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false + }, + { + "id": "dZCGnL5uL_GJnEQ9dNtV-", + "type": "rectangle", + "x": 792.3049442155357, + "y": 328.1905624739571, + "width": 137.2841700563661, + "height": 257.5294034200426, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b4u", + "roundness": null, + "seed": 1517783366, + "version": 2908, + "versionNonce": 1969117658, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false + }, + { + "id": "YssfYGHYK44-J9nc0lPXD", + "type": "rectangle", + "x": 788.4885192289755, + "y": 322.71612529390234, + "width": 135.30408576921187, + "height": 255.64890738600647, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b4w", + "roundness": null, + "seed": 243429510, + "version": 2555, + "versionNonce": 250220186, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false + }, + { + "id": "aNn5lWik72i0y5f8OUg38", + "type": "text", + "x": 795.9802159358646, + "y": 327.61906986530073, + "width": 60.25439153040449, + "height": 21.831316450229316, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b4y", + "roundness": null, + "seed": 657354694, + "version": 1566, + "versionNonce": 245443418, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false, + "text": "Flag set", + "fontSize": 16.171345518688376, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Flag set", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "6zZQ5QABGQBlQXBhroSd3", + "type": "text", + "x": 795.0948557966076, + "y": 357.98415399712036, + "width": 132.03518612198633, + "height": 201.79815899960101, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b4z", + "roundness": null, + "seed": 546866950, + "version": 3410, + "versionNonce": 1792454682, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false, + "text": "(created using Split\nPublic API)\n(a list of Feature\nflags)\n(optimizes SDK\ninitialization by\nlimiting the number\nof downloaded\nFeature flag\ndefinitions to specific\nFlag set(s))", + "fontSize": 13.589101616134748, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(created using Split Public API)\n(a list of Feature flags)\n(optimizes SDK initialization by limiting the number of downloaded Feature flag definitions to specific Flag set(s))", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "cuunp-Bza9S3ENrfsnWwH", + "type": "rectangle", + "x": 636.7933565892181, + "y": 327.82045551389825, + "width": 133.4477887010745, + "height": 255.94985773033224, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b50", + "roundness": null, + "seed": 1799206470, + "version": 3186, + "versionNonce": 1467849946, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false + }, + { + "id": "YpbByM_wXA-9lk3dv0GkK", + "type": "rectangle", + "x": 628.94100919027, + "y": 323.0973296519134, + "width": 135.67738840883908, + "height": 255.14177225943183, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b51", + "roundness": null, + "seed": 1610284422, + "version": 3063, + "versionNonce": 304673178, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false + }, + { + "id": "9gjZaiDVVeqWa9eQJRIXM", + "type": "text", + "x": 638.4548701845417, + "y": 328.05560206807814, + "width": 92.19120458789011, + "height": 59.995331029853624, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b53", + "roundness": null, + "seed": 873290950, + "version": 3007, + "versionNonce": 772517466, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false, + "text": "Attribute /\nTraffic type\nattribute", + "fontSize": 14.813661982679903, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Attribute / Traffic type attribute", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "7wFA9Mef1KxAl5-GaXGC9", + "type": "text", + "x": 637.2524704973684, + "y": 395.45471332621577, + "width": 128.27468237952482, + "height": 131.57559359854312, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b54", + "roundness": null, + "seed": 1829884934, + "version": 4168, + "versionNonce": 1610142490, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false, + "text": "(called Traffic type\nAttributes in FME\nAdmin settings)\n(can be used in\nFeature flag\nattribute based\ntargeting rules)", + "fontSize": 13.923343237941069, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(called Traffic type Attributes in FME Admin settings)\n(can be used in Feature flag attribute based targeting rules)", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "TBnf8bKZoyBQRl_dJOfsr", + "type": "rectangle", + "x": 958.7434685666306, + "y": 336.2815362973599, + "width": 138.7475031063966, + "height": 255.70071817713168, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b55", + "roundness": null, + "seed": 1426416454, + "version": 2960, + "versionNonce": 1248026586, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false + }, + { + "id": "URCSX4shOSkxZvQ_2qWzN", + "type": "rectangle", + "x": 956.3314165890392, + "y": 330.47944600933346, + "width": 134.86867546549277, + "height": 255.1581590782473, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b56", + "roundness": null, + "seed": 1850018438, + "version": 3047, + "versionNonce": 1230918810, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false + }, + { + "id": "tVALKQHWwBkrHclToJl32", + "type": "rectangle", + "x": 950.3444234833237, + "y": 323.0915735049985, + "width": 135.64773795299268, + "height": 255.79957186384837, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b57", + "roundness": null, + "seed": 1737345478, + "version": 3446, + "versionNonce": 1050456410, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false + }, + { + "id": "lgGtTqK9rNkd2mXLNJ_Xm", + "type": "text", + "x": 957.140632872102, + "y": 381.83512320616666, + "width": 130.37045122721545, + "height": 188.51742108673875, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b59", + "roundness": null, + "seed": 268214534, + "version": 5571, + "versionNonce": 1567716890, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false, + "text": "(called Metric\ndimension in FME\nAdmin Settings)\n(appears under\nDimensional\nAnalysis on Metric\nDetails page)\n(sent from SDK or\nintegration as event\nproperty)", + "fontSize": 13.96425341383252, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(called Metric dimension in FME Admin Settings)\n(appears under Dimensional Analysis on Metric Details page)\n(sent from SDK or integration as event property)", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "Dzb1AVG5wDjn-MYIR_6ye", + "type": "text", + "x": 957.4295818221859, + "y": 327.9001249532708, + "width": 122.02119028593634, + "height": 41.59000378520803, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5A", + "roundness": null, + "seed": 1259245638, + "version": 2963, + "versionNonce": 1993319130, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false, + "text": "Metric dimension\n/ Event property", + "fontSize": 15.403705105632605, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Metric dimension / Event property", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "N2boCyaUCUlUCq2tdBHF5", + "type": "rectangle", + "x": 650.2176438866211, + "y": 219.81272220675538, + "width": 211.43758281417283, + "height": 91.50582511156107, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5B", + "roundness": null, + "seed": 77633414, + "version": 1908, + "versionNonce": 1162491802, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false + }, + { + "id": "CrqgwU2COKOMKwr1Ke1pF", + "type": "rectangle", + "x": 646.0396836980467, + "y": 217.01375534068393, + "width": 208.77766467510324, + "height": 88.10082104964603, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5C", + "roundness": null, + "seed": 619689670, + "version": 2084, + "versionNonce": 957625434, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false + }, + { + "id": "1rR9OXE_W5SjZkRPtd0OB", + "type": "rectangle", + "x": 642.1856261707753, + "y": 212.8945641110089, + "width": 206.64278117596578, + "height": 85.13205900473349, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5D", + "roundness": null, + "seed": 77131270, + "version": 1703, + "versionNonce": 1140889882, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false + }, + { + "id": "ErlRj2C0w7bKrttVi7EY3", + "type": "text", + "x": 651.3409825240502, + "y": 219.5098599048908, + "width": 174.56216379821603, + "height": 23.540012410807172, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5E", + "roundness": null, + "seed": 1620655430, + "version": 930, + "versionNonce": 629302746, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false, + "text": "Feature flag metadata", + "fontSize": 17.437046230227534, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Feature flag metadata", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "0S13Y0QlI5473m9kTAIoI", + "type": "text", + "x": 650.028515433051, + "y": 252.60491575617598, + "width": 191.82507695867696, + "height": 38.428947931699845, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5F", + "roundness": null, + "seed": 1313711238, + "version": 1630, + "versionNonce": 917281434, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false, + "text": "(Name, Traffic type, Owners,\nTags, Description)", + "fontSize": 14.23294367840735, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(Name, Traffic type, Owners, Tags, Description)", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "fC7UgzXRvKYB9NrEqc84H", + "type": "rectangle", + "x": 889.6159510675101, + "y": 219.27294815627448, + "width": 194.87139894375372, + "height": 89.78015978766544, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5G", + "roundness": null, + "seed": 796074950, + "version": 1960, + "versionNonce": 909260634, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false + }, + { + "id": "ToPJPcTb7y2BmfbQHYqGX", + "type": "rectangle", + "x": 885.5609867805387, + "y": 215.15584812328055, + "width": 193.0453593504589, + "height": 87.73792946028072, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5H", + "roundness": null, + "seed": 640072454, + "version": 2094, + "versionNonce": 342244378, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false + }, + { + "id": "N9rWuEUut_-hUiSiefe3K", + "type": "rectangle", + "x": 882.3991683097992, + "y": 212.0298011746495, + "width": 190.13064498906937, + "height": 84.48348680246396, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5I", + "roundness": null, + "seed": 149252678, + "version": 1861, + "versionNonce": 1060874458, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false + }, + { + "id": "QakraGoDz8vigyCEj-koJ", + "type": "text", + "x": 891.7272157897803, + "y": 219.12212007142077, + "width": 149.11755915989656, + "height": 23.34286527541397, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5J", + "roundness": null, + "seed": 1452028294, + "version": 1125, + "versionNonce": 1850047898, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false, + "text": "Segment metadata", + "fontSize": 17.291011315121462, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Segment metadata", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "s5dWjiO7niu4GCU4zuw-S", + "type": "text", + "x": 890.0136249855219, + "y": 251.89084718745423, + "width": 175.90089593021492, + "height": 38.451356384121034, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5K", + "roundness": null, + "seed": 710414534, + "version": 1612, + "versionNonce": 1857469018, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false, + "text": "(Name, Traffic type,\nOwners, Tags, Description)", + "fontSize": 14.241243105230009, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(Name, Traffic type, Owners, Tags, Description)", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "u1bD2sdhFUiZocrWEAhOo", + "type": "rectangle", + "x": 1121.9604397158769, + "y": 218.58141451991582, + "width": 224.96182207401438, + "height": 375.0552617474032, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5L", + "roundness": null, + "seed": 803604486, + "version": 2136, + "versionNonce": 1868717850, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false + }, + { + "id": "FFyxquS6722Kj2Uxbi0jB", + "type": "rectangle", + "x": 1118.7088138178347, + "y": 213.44512243301483, + "width": 222.1865542035298, + "height": 374.20980155515923, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5M", + "roundness": null, + "seed": 389224262, + "version": 2501, + "versionNonce": 1287351258, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false + }, + { + "id": "arJu28qbib5ZrxZgzXvBV", + "type": "rectangle", + "x": 1115.2046718844315, + "y": 210.11472632021093, + "width": 219.93084502230602, + "height": 371.7506319548874, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5N", + "roundness": null, + "seed": 144457350, + "version": 2143, + "versionNonce": 1557455002, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false + }, + { + "id": "iZ8v3DPkAdncN1r8VwmRe", + "type": "rectangle", + "x": 1136.1991613277514, + "y": 491.1551125071233, + "width": 184.1027013454992, + "height": 68.6436359906346, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5O", + "roundness": null, + "seed": 417234374, + "version": 1603, + "versionNonce": 996261210, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false + }, + { + "id": "Qx_HmD9Erg0Xy6aivPntX", + "type": "rectangle", + "x": 1130.3593175777517, + "y": 484.64571680624675, + "width": 182.4935619559986, + "height": 68.60870885965744, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5P", + "roundness": null, + "seed": 1024906502, + "version": 1544, + "versionNonce": 364184090, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false + }, + { + "id": "bZpqSb5WR72p2L5kKlOSj", + "type": "text", + "x": 1130.183701576886, + "y": 216.03760839255432, + "width": 164.753562902967, + "height": 26.024398625429583, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5Q", + "roundness": null, + "seed": 1052918854, + "version": 1412, + "versionNonce": 1992404698, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false, + "text": "Metric metadata", + "fontSize": 19.277332315133034, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Metric metadata", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "cGvkbJQpnG83DkTeHazqA", + "type": "text", + "x": 1127.9221449053339, + "y": 251.90090780574184, + "width": 135.63023039193214, + "height": 58.2566975800916, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5R", + "roundness": null, + "seed": 800967558, + "version": 2533, + "versionNonce": 320521114, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false, + "text": "(Name, Traffic type,\nOwners, Tags,\nDescription)", + "fontSize": 14.384369772862119, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(Name, Traffic type, Owners, Tags, Description)", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "9A1cSyigAeaWfUo-_vqVH", + "type": "rectangle", + "x": 1127.3954142763612, + "y": 331.8076898348738, + "width": 178.31721877556808, + "height": 133.83114621280322, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5S", + "roundness": null, + "seed": 1581673158, + "version": 2578, + "versionNonce": 351486042, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false + }, + { + "id": "jQHlMDWIzCd3GoMARNKlY", + "type": "text", + "x": 1137.82191564937, + "y": 339.22910290800615, + "width": 130.30189718031852, + "height": 23.13672888360233, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5T", + "roundness": null, + "seed": 1224286726, + "version": 1862, + "versionNonce": 1746806042, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false, + "text": "Metric definition", + "fontSize": 17.13831769155728, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Metric definition", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "L7FgmsexjsX6akQ_HRF2g", + "type": "rectangle", + "x": 1125.5919187722895, + "y": 481.74689851713475, + "width": 180.01395417075696, + "height": 65.80755935763469, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5U", + "roundness": null, + "seed": 315814214, + "version": 2542, + "versionNonce": 101609946, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false + }, + { + "id": "9z95CWWEHVtU0qWaLQWwe", + "type": "text", + "x": 1137.4306846298334, + "y": 491.6370963419029, + "width": 154.48103945591106, + "height": 21.6, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5V", + "roundness": null, + "seed": 1999689862, + "version": 1881, + "versionNonce": 1104619162, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false, + "text": "Alert policy", + "fontSize": 16, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Alert policy", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "ANIj0tJHBU7BxXOXPv6Qv", + "type": "text", + "x": 1136.103680819423, + "y": 367.4825286921832, + "width": 163.84178325823396, + "height": 84.85052111618968, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5W", + "roundness": null, + "seed": 1037031366, + "version": 2748, + "versionNonce": 338379610, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605636, + "link": null, + "locked": false, + "text": "(Select desired impact\n[increase|decrease],\nMeasure as [metric\ntype], Filter by, Cap at)", + "fontSize": 15.71305946596105, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(Select desired impact [increase|decrease], Measure as [metric type], Filter by, Cap at)", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "FzzHS4fIZc9587iAVykgM", + "type": "rectangle", + "x": 478.50876250892907, + "y": 634.2476404833361, + "width": 1044.7052569452653, + "height": 863.2084417652303, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5b", + "roundness": null, + "seed": 785182470, + "version": 2980, + "versionNonce": 65901049, + "isDeleted": false, + "boundElements": [], + "updated": 1739350277656, + "link": null, + "locked": false + }, + { + "id": "foAljznGJ2D_cJ3SyYw-k", + "type": "rectangle", + "x": 472.39304038727164, + "y": 625.9748155913626, + "width": 1042.992386475524, + "height": 862.8501498941741, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5c", + "roundness": null, + "seed": 111484486, + "version": 2733, + "versionNonce": 686253591, + "isDeleted": false, + "boundElements": [], + "updated": 1739350277656, + "link": null, + "locked": false + }, + { + "id": "VHvlYyARhoIXa7Yzj5CLA", + "type": "rectangle", + "x": 1163.7145554907568, + "y": 795.4507077346695, + "width": 215.90068893780858, + "height": 118.69554701876261, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5d", + "roundness": null, + "seed": 1781884294, + "version": 1958, + "versionNonce": 367900998, + "isDeleted": false, + "boundElements": [], + "updated": 1739279841321, + "link": null, + "locked": false + }, + { + "id": "auIb_LOsjg7CrqFZTR3f9", + "type": "rectangle", + "x": 1155.606595288324, + "y": 788.9922376890903, + "width": 218.41540343658934, + "height": 122.04128262788265, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5e", + "roundness": null, + "seed": 1522189510, + "version": 2120, + "versionNonce": 1406789146, + "isDeleted": false, + "boundElements": [], + "updated": 1739279841321, + "link": null, + "locked": false + }, + { + "id": "4pnqb_JrXCNZsJFZ5_q3m", + "type": "rectangle", + "x": 904.0907870733217, + "y": 796.6584605868392, + "width": 228.70570747523183, + "height": 152.52727994074846, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5f", + "roundness": null, + "seed": 701643782, + "version": 1990, + "versionNonce": 1538433158, + "isDeleted": false, + "boundElements": [], + "updated": 1739279841321, + "link": null, + "locked": false + }, + { + "id": "xpr1w6-lhxAZqCvs7smtd", + "type": "rectangle", + "x": 898.7358426904909, + "y": 790.3298748868173, + "width": 229.5311785917195, + "height": 156.25168849156017, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5g", + "roundness": null, + "seed": 1836632902, + "version": 2178, + "versionNonce": 1722421978, + "isDeleted": false, + "boundElements": [], + "updated": 1739279841321, + "link": null, + "locked": false + }, + { + "id": "YzMxGTIWBvIWMQO1O9Jox", + "type": "rectangle", + "x": 463.0245327323861, + "y": 620.022084779684, + "width": 1045.0007435505383, + "height": 861.3871247540291, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5h", + "roundness": null, + "seed": 1946907270, + "version": 2597, + "versionNonce": 1211820761, + "isDeleted": false, + "boundElements": [], + "updated": 1739350277656, + "link": null, + "locked": false + }, + { + "id": "kOqFz-5Mly_AcO324U_JZ", + "type": "line", + "x": 463.59955578649124, + "y": 657.7849979898618, + "width": 1043.9198661409614, + "height": 1.8006425030454238, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5i", + "roundness": null, + "seed": 944189894, + "version": 2182, + "versionNonce": 810985370, + "isDeleted": false, + "boundElements": [], + "updated": 1739279841321, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 1043.9198661409614, + -1.8006425030454238 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "On_Cq_9sgfoi7CMY1PDtC", + "type": "text", + "x": 481.56788887718767, + "y": 627.6922895936219, + "width": 125.89466285705566, + "height": 29.229974054055496, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5j", + "roundness": null, + "seed": 1430849798, + "version": 1165, + "versionNonce": 595375878, + "isDeleted": false, + "boundElements": [], + "updated": 1739279841321, + "link": null, + "locked": false, + "text": "Environment", + "fontSize": 21.6518326326337, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Environment", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "65cEzZYrc7Y0ojBwRd9nu", + "type": "text", + "x": 622.7093434483261, + "y": 629.6961614190482, + "width": 565.653297662735, + "height": 23.383979243244397, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5k", + "roundness": null, + "seed": 2056124486, + "version": 1347, + "versionNonce": 1762829402, + "isDeleted": false, + "boundElements": [], + "updated": 1739279841321, + "link": null, + "locked": false, + "text": "(allows separate control and data collection for staging, production, etc.)", + "fontSize": 17.32146610610696, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(allows separate control and data collection for staging, production, etc.)", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "8-KwaSgjGrZePHHBqdxpn", + "type": "text", + "x": 489.92372305842645, + "y": 674.074521360994, + "width": 275.46101165429326, + "height": 92.4173867240141, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5l", + "roundness": null, + "seed": 1609760646, + "version": 705, + "versionNonce": 971746903, + "isDeleted": false, + "boundElements": [], + "updated": 1739349337661, + "link": null, + "locked": false, + "text": "Change permissions\n \n \n Approval required", + "fontSize": 17.114330874817423, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Change permissions\n \n \n Approval required", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "gsJHZCfeDCVGf-CxQ7WjQ", + "type": "ellipse", + "x": 490.8272368629725, + "y": 704.7094302665438, + "width": 10.066137933221853, + "height": 10.429207181753382, + "angle": 0, + "strokeColor": "#6741d9", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5m", + "roundness": null, + "seed": 968171206, + "version": 645, + "versionNonce": 195937785, + "isDeleted": false, + "boundElements": [], + "updated": 1739349126593, + "link": null, + "locked": false + }, + { + "id": "LjutmZntVDH8nqMox07yC", + "type": "ellipse", + "x": 490.8272368629725, + "y": 727.266735696477, + "width": 10.066137933221853, + "height": 10.429207181753382, + "angle": 0, + "strokeColor": "#6741d9", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5p", + "roundness": null, + "seed": 745625094, + "version": 671, + "versionNonce": 594833943, + "isDeleted": false, + "boundElements": [], + "updated": 1739349126593, + "link": null, + "locked": false + }, + { + "id": "UL8YGFhXTpIQBgNR93nH1", + "type": "ellipse", + "x": 490.8272368629725, + "y": 750.4665476427406, + "width": 10.066137933221853, + "height": 10.429207181753382, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5q", + "roundness": null, + "seed": 1092920646, + "version": 810, + "versionNonce": 484710839, + "isDeleted": false, + "boundElements": [], + "updated": 1739349270189, + "link": null, + "locked": false + }, + { + "id": "6rPbvC3k15yvg8ue3xnUX", + "type": "text", + "x": 778.8194356560932, + "y": 676.5141234286987, + "width": 335.5510134884901, + "height": 69.31304004301057, + "angle": 0, + "strokeColor": "#6741d9", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5r", + "roundness": null, + "seed": 1496059014, + "version": 1571, + "versionNonce": 726220377, + "isDeleted": false, + "boundElements": [], + "updated": 1739349885856, + "link": null, + "locked": false, + "text": "Data export permissions (Split Legacy)\n Any login user\n Restricted", + "fontSize": 17.114330874817423, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Data export permissions (Split Legacy)\n Any login user\n Restricted", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "6eKyqcTcacqYLEoMYWOpF", + "type": "ellipse", + "x": 779.7220818703639, + "y": 706.7976338604708, + "width": 10.066137933221853, + "height": 10.429207181753382, + "angle": 0, + "strokeColor": "#6741d9", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5s", + "roundness": null, + "seed": 574917574, + "version": 1291, + "versionNonce": 890394231, + "isDeleted": false, + "boundElements": [], + "updated": 1739349484338, + "link": null, + "locked": false + }, + { + "id": "zauS_WUpUoZocePdK0T82", + "type": "ellipse", + "x": 779.7220818703639, + "y": 729.3549392904042, + "width": 10.066137933221853, + "height": 10.429207181753382, + "angle": 0, + "strokeColor": "#6741d9", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5t", + "roundness": null, + "seed": 176890630, + "version": 1317, + "versionNonce": 482193529, + "isDeleted": false, + "boundElements": [], + "updated": 1739349484338, + "link": null, + "locked": false + }, + { + "id": "7C83o0Ug2UbwLkJjrVDVt", + "type": "text", + "x": 1138.596761750577, + "y": 676.2322004144642, + "width": 152.5868263244629, + "height": 69.31304004301057, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5v", + "roundness": null, + "seed": 880595526, + "version": 1275, + "versionNonce": 120574105, + "isDeleted": false, + "boundElements": [], + "updated": 1739350476768, + "link": null, + "locked": false, + "text": "Environment type\n Production\n Pre-production", + "fontSize": 17.114330874817423, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Environment type\n Production\n Pre-production", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "_YKKu3hg__P3A4DEx5Vw-", + "type": "ellipse", + "x": 1140.288725710869, + "y": 707.398814365645, + "width": 10.066137933221853, + "height": 10.429207181753382, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5w", + "roundness": null, + "seed": 1794546054, + "version": 1172, + "versionNonce": 551936375, + "isDeleted": false, + "boundElements": [], + "updated": 1739350476768, + "link": null, + "locked": false + }, + { + "id": "CJclu0-02bkUjXy7Z3LB9", + "type": "ellipse", + "x": 1140.288725710869, + "y": 728.854532010729, + "width": 10.066137933221853, + "height": 10.429207181753382, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5x", + "roundness": null, + "seed": 355825862, + "version": 1196, + "versionNonce": 838620537, + "isDeleted": false, + "boundElements": [], + "updated": 1739350476768, + "link": null, + "locked": false + }, + { + "id": "bBZvNKxmwIPtAonr6biq6", + "type": "rectangle", + "x": 486.56851769976834, + "y": 786.3513692439795, + "width": 162.296875, + "height": 154.21165025578128, + "angle": 0, + "strokeColor": "#6741d9", + "backgroundColor": "#d0bfff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5y", + "roundness": null, + "seed": 718696454, + "version": 1220, + "versionNonce": 1682258566, + "isDeleted": false, + "boundElements": [], + "updated": 1739279841321, + "link": null, + "locked": false + }, + { + "id": "iE9XVBkGFf7zBS-XqUsgO", + "type": "text", + "x": 496.1791797258702, + "y": 793.3381672087588, + "width": 114.69634354114532, + "height": 45.44715568856511, + "angle": 0, + "strokeColor": "#6741d9", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b5z", + "roundness": null, + "seed": 668368710, + "version": 1348, + "versionNonce": 1504057562, + "isDeleted": false, + "boundElements": [], + "updated": 1739279841321, + "link": null, + "locked": false, + "text": "Admin API Key\n(Split Legacy)", + "fontSize": 16.832279884653744, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Admin API Key\n(Split Legacy)", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "zPETT1JNizQx8nD3aanpe", + "type": "text", + "x": 495.32012683644643, + "y": 845.77154541965, + "width": 157.19163689505626, + "height": 87.65209462040332, + "angle": 0, + "strokeColor": "#6741d9", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b60", + "roundness": null, + "seed": 9610886, + "version": 1504, + "versionNonce": 235403718, + "isDeleted": false, + "boundElements": [], + "updated": 1739279841321, + "link": null, + "locked": false, + "text": "(if scoped to\nenvironment)\n(used with Split\nPublic API)", + "fontSize": 16.23186937414877, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(if scoped to environment)\n(used with Split Public API)", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "_QoNf92jBR_7dSe4EyJah", + "type": "rectangle", + "x": 687.55242876981, + "y": 800.8767395220949, + "width": 188.89273264294542, + "height": 112.96287602352916, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b61", + "roundness": null, + "seed": 365422022, + "version": 1476, + "versionNonce": 836890010, + "isDeleted": false, + "boundElements": [], + "updated": 1739279841321, + "link": null, + "locked": false + }, + { + "id": "dfNo_Mw6Ic9Qrgo1nUJBW", + "type": "rectangle", + "x": 682.9046358338472, + "y": 792.7213109371083, + "width": 188.2525912623597, + "height": 116.30849662814302, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b62", + "roundness": null, + "seed": 680738054, + "version": 1738, + "versionNonce": 1202770182, + "isDeleted": false, + "boundElements": [], + "updated": 1739279841321, + "link": null, + "locked": false + }, + { + "id": "o_VMSpZFLcRFk2aU5Mc2A", + "type": "rectangle", + "x": 677.0590603132636, + "y": 786.3927252370867, + "width": 188.64190823060235, + "height": 118.20111226890205, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b63", + "roundness": null, + "seed": 1316072518, + "version": 1907, + "versionNonce": 1699031642, + "isDeleted": false, + "boundElements": [], + "updated": 1739279841321, + "link": null, + "locked": false + }, + { + "id": "WulU5vCrpmZHEJZWVchRY", + "type": "text", + "x": 688.7918356722311, + "y": 794.4769697581493, + "width": 100.3068641948537, + "height": 23.167558377365832, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b64", + "roundness": null, + "seed": 1698706310, + "version": 865, + "versionNonce": 1555873862, + "isDeleted": false, + "boundElements": [], + "updated": 1739279841321, + "link": null, + "locked": false, + "text": "SDK API Key", + "fontSize": 17.161154353604307, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "SDK API Key", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "fEHXsuz8occXUbJtOUG5Q", + "type": "text", + "x": 686.4793685812318, + "y": 824.5308682935914, + "width": 164.012054424664, + "height": 44.903182019435306, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b65", + "roundness": null, + "seed": 121361094, + "version": 1518, + "versionNonce": 1825820825, + "isDeleted": false, + "boundElements": [], + "updated": 1739349883468, + "link": null, + "locked": false, + "text": "(always scoped to\none environment)", + "fontSize": 16.630808155346408, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(always scoped to one environment)", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "d25wBqdACiotGZwMd2tfI", + "type": "rectangle", + "x": 897.3327682487324, + "y": 785.485157197173, + "width": 230.41845325143808, + "height": 223.24627366495008, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b66", + "roundness": null, + "seed": 2130807302, + "version": 2103, + "versionNonce": 1063131737, + "isDeleted": false, + "boundElements": [], + "updated": 1739350162769, + "link": null, + "locked": false + }, + { + "id": "62Av9XawMjW_FEZ1ToPMG", + "type": "text", + "x": 911.5063232426305, + "y": 793.7377979415071, + "width": 180.81082293645244, + "height": 24.537070446735324, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b67", + "roundness": null, + "seed": 999184710, + "version": 1224, + "versionNonce": 1718475738, + "isDeleted": false, + "boundElements": [], + "updated": 1739279841321, + "link": null, + "locked": false, + "text": "Feature flag definition", + "fontSize": 18.17560773832247, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Feature flag definition", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "i_CL9WxSQBz-YIzgZDf5L", + "type": "text", + "x": 911.1443473655324, + "y": 888.6560837207894, + "width": 207.32013061536216, + "height": 108, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b68", + "roundness": null, + "seed": 1144995974, + "version": 1947, + "versionNonce": 1440692313, + "isDeleted": false, + "boundElements": [], + "updated": 1739350176546, + "link": null, + "locked": false, + "text": "(Treatments [variations],\nTargeting rules, Status,\nEditing permission\noverrides, Key metrics,\nSupporting metrics)", + "fontSize": 16, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(Treatments [variations], Targeting rules, Status, Editing permission overrides, Key metrics, Supporting metrics)", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "gcJ6wvROhpqcgNImTU9yL", + "type": "rectangle", + "x": 1151.676331723495, + "y": 784.9427342448657, + "width": 216.13790242641505, + "height": 202.01863518039207, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b69", + "roundness": null, + "seed": 751910854, + "version": 1885, + "versionNonce": 1687787607, + "isDeleted": false, + "boundElements": [], + "updated": 1739350200249, + "link": null, + "locked": false + }, + { + "id": "PpTjabmQS6gVUXj98xx5b", + "type": "text", + "x": 1162.823588579576, + "y": 793.6326828165685, + "width": 153.4058927564789, + "height": 24.188058419244175, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b6A", + "roundness": null, + "seed": 157374214, + "version": 1208, + "versionNonce": 1756700166, + "isDeleted": false, + "boundElements": [], + "updated": 1739279841321, + "link": null, + "locked": false, + "text": "Segment definition", + "fontSize": 17.91708031055124, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Segment definition", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "eB6hdxiAj4VJOA1hrx6Q3", + "type": "rectangle", + "x": 1210.7732543918755, + "y": 892.1050589714669, + "width": 38.19755729821463, + "height": 18.46095262607082, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b6B", + "roundness": null, + "seed": 1651279430, + "version": 645, + "versionNonce": 1740404791, + "isDeleted": false, + "boundElements": [], + "updated": 1739350213060, + "link": null, + "locked": false + }, + { + "id": "HW9WWilpKbeVaFoVDO8Sc", + "type": "text", + "x": 1163.0360427980643, + "y": 890.4026110707593, + "width": 203.08792229074453, + "height": 86.4, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b6C", + "roundness": null, + "seed": 684707206, + "version": 2733, + "versionNonce": 907581913, + "isDeleted": false, + "boundElements": [], + "updated": 1739350196528, + "link": null, + "locked": false, + "text": "(list of keys, or identifiers\nfor end users of your app,\nEditing permission\noverrides)", + "fontSize": 16, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(list of keys, or identifiers for end users of your app, Editing permission overrides)", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "XOXdyXQuZ9ZCLLGMdMZzC", + "type": "rectangle", + "x": 525.2972149249115, + "y": 1066.7374461301517, + "width": 948.3525569044615, + "height": 373.29657602364614, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b6r", + "roundness": null, + "seed": 700984518, + "version": 1249, + "versionNonce": 316101177, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false + }, + { + "id": "6wGKVcq4Ktv6RArdfRwTI", + "type": "rectangle", + "x": 516.2479442504114, + "y": 1059.1502067614188, + "width": 947.7348690927762, + "height": 373.29657602364614, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b7W", + "roundness": null, + "seed": 49456134, + "version": 1433, + "versionNonce": 648595927, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false + }, + { + "id": "qjX9vTW2n3tOddUIEaigj", + "type": "rectangle", + "x": 505.5626605178058, + "y": 1050.267710932084, + "width": 949.0435951437843, + "height": 373.29657602364614, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b7X", + "roundness": null, + "seed": 374092614, + "version": 844, + "versionNonce": 209888025, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false + }, + { + "id": "_B-plC8k07-ZrhQlV1SjC", + "type": "line", + "x": 507.8116449344153, + "y": 1085.641311946378, + "width": 945.562216886453, + "height": 1.5167397596521823, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b7Z", + "roundness": null, + "seed": 2117196422, + "version": 2677, + "versionNonce": 1824487159, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 945.562216886453, + -1.5167397596521823 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "I_6r6hzTo8LJ1gPZDcZ9x", + "type": "rectangle", + "x": 657.8869888724068, + "y": 1057.0835691384445, + "width": 92.21285900798352, + "height": 21.401865164583914, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#f8f9fa", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b7ZV", + "roundness": null, + "seed": 23108038, + "version": 946, + "versionNonce": 1515247609, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false + }, + { + "id": "rDIm4lHIWAXLu1UNVMuBd", + "type": "rectangle", + "x": 542.2757610783353, + "y": 1057.9739703667888, + "width": 82.91300448983145, + "height": 21.362421669886892, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#f8f9fa", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b7a", + "roundness": null, + "seed": 116748550, + "version": 831, + "versionNonce": 1371766807, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false + }, + { + "id": "0cr-p_aZ1WhugBchAhfyQ", + "type": "text", + "x": 514.5153462541887, + "y": 1057.4529324641926, + "width": 334.1277778148651, + "height": 21.6, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b7b", + "roundness": null, + "seed": 174940230, + "version": 1347, + "versionNonce": 1044738265, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false, + "text": "per Traffic type and Environment data pipeline", + "fontSize": 16, + "fontFamily": 6, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "per Traffic type and Environment data pipeline", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "4yw81UNgU1rCORZbk4stH", + "type": "rectangle", + "x": 523.9542141991748, + "y": 1112.8233135766554, + "width": 322.86495281700456, + "height": 278.6516910012911, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "dotted", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b7c", + "roundness": null, + "seed": 262498182, + "version": 779, + "versionNonce": 1202283831, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false + }, + { + "id": "5r_Tqg0swPEy2_bQHDB9H", + "type": "line", + "x": 527.401987273339, + "y": 1146.1264706835032, + "width": 321.67248430894335, + "height": 1.5101659077943168, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "dotted", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b7d", + "roundness": null, + "seed": 383420102, + "version": 3038, + "versionNonce": 1844702649, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 321.67248430894335, + -1.5101659077943168 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "oUh0Hs3MhbtN_FUIh1iDF", + "type": "text", + "x": 624.8347850601181, + "y": 1117.8495869650078, + "width": 122.8752022449216, + "height": 25.403009477994473, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b7e", + "roundness": null, + "seed": 279559686, + "version": 982, + "versionNonce": 232105559, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false, + "text": "Data collected", + "fontSize": 18.81704405777368, + "fontFamily": 6, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Data collected", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "xHR2C7D3r52YiH7e3HMrj", + "type": "ellipse", + "x": 536.6609057550583, + "y": 1168.6444629606367, + "width": 296.25935563156884, + "height": 19.69077613476543, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b7f", + "roundness": null, + "seed": 1376783686, + "version": 736, + "versionNonce": 545246873, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false + }, + { + "id": "j2xPpmJmh8O0B7n74xDj6", + "type": "ellipse", + "x": 536.6609057550584, + "y": 1234.3846054413043, + "width": 296.25935563156884, + "height": 19.69077613476543, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b7g", + "roundness": null, + "seed": 141194374, + "version": 867, + "versionNonce": 52216695, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false + }, + { + "id": "OW5uyLBOvfL3JmWKUdsq-", + "type": "ellipse", + "x": 537.6609057550584, + "y": 1282.63377801971, + "width": 297.08937362852095, + "height": 19.69077613476543, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b7h", + "roundness": null, + "seed": 762704838, + "version": 871, + "versionNonce": 1553765241, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false + }, + { + "id": "SAorSlU0M51LJxoS93tFD", + "type": "ellipse", + "x": 536.6609057550584, + "y": 1353.2063970197937, + "width": 296.25935563156884, + "height": 19.69077613476543, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b7i", + "roundness": null, + "seed": 2068810502, + "version": 891, + "versionNonce": 935541911, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false + }, + { + "id": "fkQ22j67eZEJGu_c2YnKN", + "type": "line", + "x": 537.124037483747, + "y": 1177.7151831058018, + "width": 0.5961650229149882, + "height": 66.24710513356285, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b7k", + "roundness": null, + "seed": 1618229830, + "version": 616, + "versionNonce": 109952089, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.5961650229149882, + 66.24710513356285 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "VzS6YWF6PJe-guC5XLUCL", + "type": "line", + "x": 835.7175953460737, + "y": 1179.4199620910258, + "width": 0.6620204614927161, + "height": 63.32520330613693, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b7l", + "roundness": null, + "seed": 1942028678, + "version": 870, + "versionNonce": 1065193911, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.6620204614927161, + 63.32520330613693 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "StgdujzXHoOH3VvBOdSuC", + "type": "line", + "x": 537.3054058660856, + "y": 1294.4918154985885, + "width": 0.8387903229384506, + "height": 69.10315152241105, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b7m", + "roundness": null, + "seed": 1752359110, + "version": 962, + "versionNonce": 774293817, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.8387903229384506, + 69.10315152241105 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "DcQPI0c1N9v_Hir93fqGS", + "type": "line", + "x": 834.9286944490725, + "y": 1296.1353295308172, + "width": 1.0640852443889344, + "height": 66.0010137578247, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b7n", + "roundness": null, + "seed": 36100102, + "version": 956, + "versionNonce": 1131430615, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -1.0640852443889344, + 66.0010137578247 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "j9W6vQQvYAnJmrhmFpoCU", + "type": "text", + "x": 636.3186449607658, + "y": 1189.052270892349, + "width": 87.35993134975433, + "height": 21.6, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b7o", + "roundness": null, + "seed": 148498246, + "version": 1242, + "versionNonce": 483080729, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false, + "text": "Impressions", + "fontSize": 16, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Impressions", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "JvJBOb0L9NRFoO-toPso5", + "type": "text", + "x": 658.2786125143814, + "y": 1303.7413245261278, + "width": 49.18396699428558, + "height": 21.6, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b7p", + "roundness": null, + "seed": 1021906566, + "version": 1355, + "versionNonce": 1412419575, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false, + "text": "Events", + "fontSize": 16, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Events", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "hNQIgob1OtZuQG1CbyR_t", + "type": "rectangle", + "x": 538.4788624673772, + "y": 1214.695042155753, + "width": 292.9593099545348, + "height": 29.297380114207957, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b7q", + "roundness": null, + "seed": 1259425222, + "version": 1255, + "versionNonce": 594448121, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false + }, + { + "id": "ulB9ACyu4BBw1ymrSepsp", + "type": "rectangle", + "x": 539.1104420029125, + "y": 1337.103689942127, + "width": 292.8383389012374, + "height": 25.104786401004503, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b7s", + "roundness": null, + "seed": 1081058566, + "version": 1707, + "versionNonce": 2076227863, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false + }, + { + "id": "SksWec6vEIt8zhdvg0tXy", + "type": "text", + "x": 541.7817446345384, + "y": 1207.3028656012375, + "width": 287.6522537644755, + "height": 42.03778021664115, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b7t", + "roundness": null, + "seed": 2122388550, + "version": 2644, + "versionNonce": 680806361, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false, + "text": "(logs of rule evaluations, sent from SDK\nor Split Evaluator)", + "fontSize": 15.569548228385607, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(logs of rule evaluations, sent from SDK or Split Evaluator)", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "N59csCQ3kB1qyacr5PudW", + "type": "text", + "x": 542.1470678006331, + "y": 1323.0535929744105, + "width": 304.6860235849729, + "height": 43.2, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b7u", + "roundness": null, + "seed": 790797190, + "version": 2497, + "versionNonce": 261586487, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false, + "text": "(performance and behavioral data, sent\nfrom SDK, API, or integrations)", + "fontSize": 16, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(performance and behavioral data, sent from SDK, API, or integrations)", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "7HgrUOYtoRX1AQjrSiRM9", + "type": "line", + "x": 867.4393572056623, + "y": 1183.4921785671831, + "width": 200.48128540942957, + "height": 0.14557518001424796, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b7w", + "roundness": null, + "seed": 949041862, + "version": 666, + "versionNonce": 1984947385, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 200.48128540942957, + 0.14557518001424796 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "snBNpuFbFziQAEReF4cDV", + "type": "line", + "x": 865.5780745469106, + "y": 1291.5921479548028, + "width": 197.32022435769454, + "height": 1.7434360844549701, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b7z", + "roundness": null, + "seed": 829026822, + "version": 685, + "versionNonce": 1117298519, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 197.32022435769454, + -1.7434360844549701 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "9YJgvMPh1Zx28BYW64Gmc", + "type": "line", + "x": 1068.278165742606, + "y": 1184.5513756033897, + "width": 48.762588460183906, + "height": 51.585229858286084, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b81", + "roundness": null, + "seed": 1420411206, + "version": 796, + "versionNonce": 1212474777, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 48.762588460183906, + 51.585229858286084 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "lCu5PX1QKzES5ekNK-n8_", + "type": "line", + "x": 1116.9716515627076, + "y": 1238.3341353353571, + "width": 53.92446016292024, + "height": 51.270305358964606, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b82", + "roundness": null, + "seed": 1974315142, + "version": 846, + "versionNonce": 1998101623, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -53.92446016292024, + 51.270305358964606 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "kVknrB-UocrYim3s2s_li", + "type": "line", + "x": 867.1274103913463, + "y": 1185.392398843227, + "width": 49.117758951903056, + "height": 53.48848042804116, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b83", + "roundness": null, + "seed": 1949476806, + "version": 903, + "versionNonce": 430361209, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 49.117758951903056, + 53.48848042804116 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "BDPfUzqcSWRKWTXpUk3NX", + "type": "line", + "x": 916.0372048003721, + "y": 1239.1634759110048, + "width": 52.642757953673254, + "height": 54.08464545095558, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b84", + "roundness": null, + "seed": 810925830, + "version": 935, + "versionNonce": 1356592535, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -52.642757953673254, + 54.08464545095558 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "yc6VFjNmR7Y8Cm1_ae5GW", + "type": "text", + "x": 925.9459874867016, + "y": 1186.8668059752968, + "width": 100.86063310722847, + "height": 27.752608715469282, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b85", + "roundness": null, + "seed": 2123624006, + "version": 1571, + "versionNonce": 859378521, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false, + "text": "Attribution", + "fontSize": 20.557487937384646, + "fontFamily": 6, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Attribution", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "Wt2ShljiVAJhWBgunP-aZ", + "type": "rectangle", + "x": 1133.8246062529074, + "y": 1119.304875162998, + "width": 300.7895165905795, + "height": 99.47637300964061, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b86", + "roundness": null, + "seed": 466236806, + "version": 697, + "versionNonce": 118958775, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "JsSVEiuOvo1RATrsiXk_1" + } + ], + "updated": 1739350261695, + "link": null, + "locked": false + }, + { + "id": "JsSVEiuOvo1RATrsiXk_1", + "type": "text", + "x": 1161.449507976046, + "y": 1155.5430616678182, + "width": 245.53971314430237, + "height": 27, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b87", + "roundness": null, + "seed": 2016711863, + "version": 578, + "versionNonce": 2035236921, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false, + "text": "Metrics impact calculations", + "fontSize": 20, + "fontFamily": 6, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "Wt2ShljiVAJhWBgunP-aZ", + "originalText": "Metrics impact calculations", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "mEscpLpWz9Z3TtAIYVEHj", + "type": "rectangle", + "x": 1137.3582704439643, + "y": 1255.5057563351638, + "width": 300.7895165905795, + "height": 99.47637300964061, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b88", + "roundness": null, + "seed": 557159622, + "version": 847, + "versionNonce": 1986968535, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "CEI39PqnhrpfiIZnIsmhC" + } + ], + "updated": 1739350261695, + "link": null, + "locked": false + }, + { + "id": "CEI39PqnhrpfiIZnIsmhC", + "type": "text", + "x": 1206.8931273110313, + "y": 1291.743942839984, + "width": 161.7198028564453, + "height": 27, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b89", + "roundness": null, + "seed": 551649303, + "version": 748, + "versionNonce": 2043466009, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false, + "text": "Alert notifications", + "fontSize": 20, + "fontFamily": 6, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "mEscpLpWz9Z3TtAIYVEHj", + "originalText": "Alert notifications", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "M1QgBcYqR_C2qD_HfSYvP", + "type": "text", + "x": 922.55172132051, + "y": 1221.4203692138612, + "width": 168.6990960361816, + "height": 60.772280534996646, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b8A", + "roundness": null, + "seed": 2020805638, + "version": 3547, + "versionNonce": 1928800503, + "isDeleted": false, + "boundElements": [], + "updated": 1739350261695, + "link": null, + "locked": false, + "text": "(associates events to\nFeature flag treatments\n[variations])", + "fontSize": 15.005501366665838, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(associates events to\nFeature flag treatments\n[variations])", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "cyV7bcMnx0jHZRPWest4U", + "type": "rectangle", + "x": 1375.9464814753028, + "y": 220.33793493366608, + "width": 190.41971922232992, + "height": 275.0865763837372, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b8B", + "roundness": null, + "seed": 1333762886, + "version": 3326, + "versionNonce": 55769434, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605637, + "link": null, + "locked": false + }, + { + "id": "PWVjedWTiGtLAtfAIfDMc", + "type": "rectangle", + "x": 1370.6412233079534, + "y": 214.3621441906531, + "width": 187.37058202158426, + "height": 274.9418690579528, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b8C", + "roundness": null, + "seed": 213402246, + "version": 3279, + "versionNonce": 1142865434, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605637, + "link": null, + "locked": false + }, + { + "id": "7ao4ssh-eB-gXXvnQfLXT", + "type": "rectangle", + "x": 1362.0659549936497, + "y": 208.55754966231302, + "width": 189.16109926351567, + "height": 274.87884262423484, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b8D", + "roundness": null, + "seed": 1121583558, + "version": 3130, + "versionNonce": 680782554, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605637, + "link": null, + "locked": false + }, + { + "id": "8mNHtz3NQnh9E570Xro01", + "type": "text", + "x": 1374.2552073041202, + "y": 213.787171883192, + "width": 128.63706184825693, + "height": 25.297313746896084, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b8E", + "roundness": null, + "seed": 620878086, + "version": 2077, + "versionNonce": 1195736986, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605637, + "link": null, + "locked": false, + "text": "Experiment", + "fontSize": 18.738750923626746, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Experiment", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "rSXLQs3chCfCJvQDZEQaF", + "type": "text", + "x": 1372.8822760400387, + "y": 253.6395669000973, + "width": 167.6912774155525, + "height": 160.49295373885022, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b8F", + "roundness": null, + "seed": 2058143814, + "version": 2839, + "versionNonce": 1818655834, + "isDeleted": false, + "boundElements": [], + "updated": 1739279605637, + "link": null, + "locked": false, + "text": "(Name, Owners, Tags,\nHypothesis, Assignment\nsource [Feature flag,\nEnvironment],\nExperiment scope [Start,\nEnd, Targeting rule,\nBaseline treatment,\nComparison treatments])", + "fontSize": 14.860458679523154, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(Name, Owners, Tags, Hypothesis, Assignment source [Feature flag, Environment], Experiment scope [Start, End, Targeting rule, Baseline treatment, Comparison treatments])", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "px__caAyiJekTOf3begqM", + "type": "text", + "x": 489.26526969270105, + "y": 697.9860851703351, + "width": 245.42745069269785, + "height": 46.20869336200705, + "angle": 0, + "strokeColor": "#6741d9", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b8G", + "roundness": null, + "seed": 84559417, + "version": 1222, + "versionNonce": 1890892855, + "isDeleted": false, + "boundElements": [], + "updated": 1739349286294, + "link": null, + "locked": false, + "text": " Any login user (Split Legacy)\n Restricted (Split Legacy)", + "fontSize": 17.114330874817423, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": " Any login user (Split Legacy)\n Restricted (Split Legacy)", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "KhRrWtdkM0OjfR36ukCxD", + "type": "text", + "x": 910.813665303507, + "y": 827.8126069965123, + "width": 164.012054424664, + "height": 44.903182019435306, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b8H", + "roundness": null, + "seed": 720221721, + "version": 1660, + "versionNonce": 1478379895, + "isDeleted": false, + "boundElements": [], + "updated": 1739350183698, + "link": null, + "locked": false, + "text": "(always scoped to\none environment)", + "fontSize": 16.630808155346408, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(always scoped to one environment)", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "G6mnWnyip1r6oY3fzPcn_", + "type": "text", + "x": 1161.8343742540242, + "y": 830.0329005585093, + "width": 164.012054424664, + "height": 44.903182019435306, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b8I", + "roundness": null, + "seed": 45412473, + "version": 1760, + "versionNonce": 548348375, + "isDeleted": false, + "boundElements": [], + "updated": 1739350222503, + "link": null, + "locked": false, + "text": "(always scoped to\none environment)", + "fontSize": 16.630808155346408, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(always scoped to one environment)", + "autoResize": false, + "lineHeight": 1.35 + } + ], + "appState": { + "gridSize": 20, + "gridStep": 5, + "gridModeEnabled": false, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} \ No newline at end of file From 85b0c789477b5b3a6b659ca3a8c0932af5e46599 Mon Sep 17 00:00:00 2001 From: lena sano Date: Wed, 12 Mar 2025 06:45:34 -0300 Subject: [PATCH 15/19] FME What's supported page review --- .../10-getting-started/whats-supported.md | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/docs/feature-management-experimentation/10-getting-started/whats-supported.md b/docs/feature-management-experimentation/10-getting-started/whats-supported.md index 347dcc72a5f..2fe2768a3f9 100644 --- a/docs/feature-management-experimentation/10-getting-started/whats-supported.md +++ b/docs/feature-management-experimentation/10-getting-started/whats-supported.md @@ -47,15 +47,15 @@ The following table lists the client-side FME SDKs that Harness supports. | [React Native](https://github.com/splitio/react-native-client) | [React Native SDK reference](https://help.split.io/hc/en-us/articles/4406066357901-React-Native-SDK) | | [Redux](https://github.com/splitio/redux-client) | [Redux SDK reference](https://help.split.io/hc/en-us/articles/360038851551-Redux-SDK) | -## Supported RUM Agents and Suite SDKs +## Supported RUM Agents and SDK Suites -RUM Agents collect Real User Metric events and send these events to Harness. Harness FME also supports FME Suite SDKs that include RUM Agents. The following table lists the FME RUM Agents and FME Suite SDKs that Harness supports. +RUM Agents collect Real User Monitoring events and send these events to Harness. Harness FME also supports FME Suite SDKs that include RUM Agents. The following table lists the FME RUM Agents and FME Suite SDKs that Harness supports. -| FME Suite SDK | FME Suite SDK documentation | RUM Agent documentation | +| FME SDK Suite | FME SDK Suite documentation | RUM Agent documentation | | ---- | --- | --- | -| [Android](https://github.com/splitio/android-client) | [Android Suite SDK reference](https://help.split.io/hc/en-us/articles/26408115004429-iOS-Suite) | [Android RUM Agent reference](https://help.split.io/hc/en-us/articles/18530305949837-Android-RUM-Agent) | -| [iOS](https://github.com/splitio/ios-client) | [iOS Suite SDK reference](https://help.split.io/hc/en-us/articles/26408115004429-iOS-Suite) | [iOS RUM Agent reference](https://help.split.io/hc/en-us/articles/22545155055373-iOS-RUM-Agent) | -| [JavaScript Browser](https://github.com/splitio/javascript-browser-client) | [JavaScript Browser Suite SDK Reference](https://help.split.io/hc/en-us/articles/22622277712781-Browser-Suite) | [JavaScript Browser RUM Agent reference](https://help.split.io/hc/en-us/articles/360030898431-Browser-RUM-Agent) | +| [Android](https://github.com/splitio/android-client) | [Android SDK Suite reference](https://help.split.io/hc/en-us/articles/26408115004429-iOS-Suite) | [Android RUM Agent reference](https://help.split.io/hc/en-us/articles/18530305949837-Android-RUM-Agent) | +| [iOS](https://github.com/splitio/ios-client) | [iOS SDK Suite reference](https://help.split.io/hc/en-us/articles/26408115004429-iOS-Suite) | [iOS RUM Agent reference](https://help.split.io/hc/en-us/articles/22545155055373-iOS-RUM-Agent) | +| [JavaScript Browser](https://github.com/splitio/javascript-browser-client) | [JavaScript Browser SDK Suite Reference](https://help.split.io/hc/en-us/articles/22622277712781-Browser-Suite) | [JavaScript Browser RUM Agent reference](https://help.split.io/hc/en-us/articles/360030898431-Browser-RUM-Agent) | ## Split Evaluator @@ -73,17 +73,20 @@ The [Split Proxy](https://help.split.io/hc/en-us/articles/4415960499213-Split-Pr This tool reduces connection latencies from the SDKs to the Harness servers transparently, and when a single connection is required from a private network to the outside for security reasons. -## Running in the Cloud +## Running in the cloud There are no limitations for using FME in any cloud or non-cloud environment as long as the languages needed are supported with an SDK, and connectivity to either Harness or the Split Proxy can be established. For information about what's supported for other Harness modules and the Harness Platform overall, go to [Supported platforms and technologies](/docs/platform/platform-whats-supported.md). + + + + + -- [Terraform provider](https://help.split.io/hc/en-us/articles/6191463919885-Terraform-provider) +- [Cloudflare Workers](https://help.split.io/hc/en-us/articles/4505572184589-Cloudflare-Workers) + -- [GitHub Actions](https://help.split.io/hc/en-us/articles/24994768544269-GitHub-Actions) -- [Jenkins](https://help.split.io/hc/en-us/articles/360044691592-Jenkins) -- [Jira Cloud](https://help.split.io/hc/en-us/articles/360059317892-Jira-Cloud) +- [Azure DevOps](https://help.split.io/hc/en-us/articles/4408032964493-Azure-DevOps) +- [ServiceNow](https://help.split.io/hc/en-us/articles/5524203735181-ServiceNow) Account(Account ID, Account name, Plan type) Admin API Key(Split Legacy)(if globally scoped)(used with SplitPublic API)User(a Harness login)User Group(a list of Harnesslogins)Tag(created in the Tag inputfield of a Segment, Featureflag, or Metric)Project(no FME data or FME relationships are shared between projects)Project permissions (Split Legacy) Anyone can access Restrict who can accessRequire comments Require title and comments forfeature flag, segment,environment, and metric changes.Traffic type(classifies the type ofkey, or uniqueidentifier, for whichfeature flag targetingdecisions are madeand data is collected)(control access rights/view permissions)Flag set(created using SplitPublic API)(a list of Featureflags)(optimizes SDKinitialization bylimiting the numberof downloadedFeature flagdefinitions to specificFlag set(s))Attribute /Traffic typeattribute(called Traffic typeAttributes in FMEAdmin settings)(can be used inFeature flagattribute basedtargeting rules)(called Metricdimension in FMEAdmin Settings)(appears underDimensionalAnalysis on MetricDetails page)(sent from SDK orintegration as eventproperty)Metric dimension/ Event propertyFeature flag metadata(Name, Traffic type, Owners,Tags, Description)Segment metadata(Name, Traffic type,Owners, Tags, Description)Metric metadata(Name, Traffic type,Owners, Tags,Description)Metric definitionAlert policy(Select desired impact[increase|decrease],Measure as [metrictype], Filter by, Cap at)Environment(allows separate control and data collection for staging, production, etc.)Change permissions Approval requiredData export permissions (Split Legacy) Any login user RestrictedEnvironment type Production Pre-productionAdmin API Key(Split Legacy)(if scoped toenvironment)(used with SplitPublic API)SDK API Key(always scoped toone environment)Feature flag definition(Treatments [variations],Targeting rules, Status,Editing permissionoverrides, Key metrics,Supporting metrics)Segment definition(list of keys, or identifiersfor end users of your app,Editing permissionoverrides)per Traffic type and Environment data pipelineData collectedImpressionsEvents(logs of rule evaluations, sent from SDKor Split Evaluator)(performance and behavioral data, sentfrom SDK, API, or integrations)AttributionMetrics impact calculationsAlert notifications(associates events toFeature flag treatments[variations])Experiment(Name, Owners, Tags,Hypothesis, Assignmentsource [Feature flag,Environment],Experiment scope [Start,End, Targeting rule,Baseline treatment,Comparison treatments]) Any login user (Split Legacy) Restricted (Split Legacy)(always scoped toone environment)(always scoped toone environment) \ No newline at end of file + @font-face { font-family: Nunito; src: url(data:font/woff2;base64,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); }Account(Account ID, Account name, Plan type) Admin API Key(Split Legacy)(if globally scoped)(used with SplitPublic API)User(a Harness login)Group(a list of Harnesslogins)Tag(created in the Tag inputfield of a Segment, Featureflag, or Metric)Project(no FME data or FME relationships are shared between projects)Project permissions (Split Legacy) Anyone can access Restrict who can accessRequire comments Require title and comments forfeature flag, segment,environment, and metric changes.Traffic type(classifies the type ofkey, or uniqueidentifier, for whichfeature flag targetingdecisions are madeand data is collected)(control access rights/view permissions)Flag set(created using SplitPublic API)(a list of Featureflags)(optimizes SDKinitialization bylimiting the numberof downloadedFeature flagdefinitions tospecific Flag set(s))Attribute /Traffic typeattribute(called Traffic typeAttributes in FMEAdmin settings)(can be used inFeature flagattribute basedtargeting rules)(called Metricdimension in FMEAdmin Settings)(appears underDimensionalAnalysis on MetricDetails page)(sent from SDK orintegration as eventproperty)Metric dimension/ Event propertyFeature flag metadata(Name, Traffic type, Owners,Tags, Description, Status)Segment metadata(Name, Traffic type,Owners, Tags, Description)Metric metadata(Name, Traffic type,Owners, Tags, Description,Metric category [Guardrailmetrics | None])Metric definitionAlert policy(Select desired impact[increase | decrease],Measure as [metrictype], Filter by, Cap at)Environment(allows separate control and data collection for staging, production, etc.)Change permissions Approval requiredData export permissions (Split Legacy) Any login user RestrictedEnvironment type Production Pre-productionAdmin API Key(Split Legacy)(if scoped toenvironment)(used with SplitPublic API)SDK API Key(always scoped toone environment)Feature flag definition(Treatments [variations],Targeting rules, Editingpermission overrides)Segment definition(list of keys or identifiers,Editing permissionoverrides)per Traffic type and Environment data pipelineData collectedImpressionsEvents(logs of rule evaluations, sent from SDKor Split Evaluator)(sent from SDK, API, or integrations)AttributionAlert notifications(associates events toFeature flag treatments[variations])Experiment(Name, Owners, Tags,Hypothesis, Assignment source[Feature flag,Environment], Scope [Start, End,Baseline treatment,Comparison treatments,Targeting rule], Keymetrics, Supportingmetrics) Any login user (Split Legacy) Restricted (Split Legacy)(always scoped toone environment)(always scoped toone environment)Metrics impact calculations(Key metrics, Supporting metrics) \ No newline at end of file diff --git a/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects-light.svg b/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects-light.svg index 59d91ec16b3..6eed0724818 100644 --- a/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects-light.svg +++ b/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects-light.svg @@ -1,2 +1,2 @@ Account(Account ID, Account name, Plan type) Admin API Key(Split Legacy)(if globally scoped)(used with SplitPublic API)User(a Harness login)User Group(a list of Harnesslogins)Tag(created in the Tag inputfield of a Segment, Featureflag, or Metric)Project(no FME data or FME relationships are shared between projects)Project permissions (Split Legacy) Anyone can access Restrict who can accessRequire comments Require title and comments forfeature flag, segment,environment, and metric changes.Traffic type(classifies the type ofkey, or uniqueidentifier, for whichfeature flag targetingdecisions are madeand data is collected)(control access rights/view permissions)Flag set(created using SplitPublic API)(a list of Featureflags)(optimizes SDKinitialization bylimiting the numberof downloadedFeature flagdefinitions to specificFlag set(s))Attribute /Traffic typeattribute(called Traffic typeAttributes in FMEAdmin settings)(can be used inFeature flagattribute basedtargeting rules)(called Metricdimension in FMEAdmin Settings)(appears underDimensionalAnalysis on MetricDetails page)(sent from SDK orintegration as eventproperty)Metric dimension/ Event propertyFeature flag metadata(Name, Traffic type, Owners,Tags, Description)Segment metadata(Name, Traffic type,Owners, Tags, Description)Metric metadata(Name, Traffic type,Owners, Tags,Description)Metric definitionAlert policy(Select desired impact[increase|decrease],Measure as [metrictype], Filter by, Cap at)Environment(allows separate control and data collection for staging, production, etc.)Change permissions Approval requiredData export permissions (Split Legacy) Any login user RestrictedEnvironment type Production Pre-productionAdmin API Key(Split Legacy)(if scoped toenvironment)(used with SplitPublic API)SDK API Key(always scoped toone environment)Feature flag definition(Treatments [variations],Targeting rules, Status,Editing permissionoverrides, Key metrics,Supporting metrics)Segment definition(list of keys, or identifiersfor end users of your app,Editing permissionoverrides)per Traffic type and Environment data pipelineData collectedImpressionsEvents(logs of rule evaluations, sent from SDKor Split Evaluator)(performance and behavioral data, sentfrom SDK, API, or integrations)AttributionMetrics impact calculationsAlert notifications(associates events toFeature flag treatments[variations])Experiment(Name, Owners, Tags,Hypothesis, Assignmentsource [Feature flag,Environment],Experiment scope [Start,End, Targeting rule,Baseline treatment,Comparison treatments]) Any login user (Split Legacy) Restricted (Split Legacy)(always scoped toone environment)(always scoped toone environment) \ No newline at end of file + @font-face { font-family: Nunito; src: url(data:font/woff2;base64,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); }Account(Account ID, Account name, Plan type) Admin API Key(Split Legacy)(if globally scoped)(used with SplitPublic API)User(a Harness login)Group(a list of Harnesslogins)Tag(created in the Tag inputfield of a Segment, Featureflag, or Metric)Project(no FME data or FME relationships are shared between projects)Project permissions (Split Legacy) Anyone can access Restrict who can accessRequire comments Require title and comments forfeature flag, segment,environment, and metric changes.Traffic type(classifies the type ofkey, or uniqueidentifier, for whichfeature flag targetingdecisions are madeand data is collected)(control access rights/view permissions)Flag set(created using SplitPublic API)(a list of Featureflags)(optimizes SDKinitialization bylimiting the numberof downloadedFeature flagdefinitions tospecific Flag set(s))Attribute /Traffic typeattribute(called Traffic typeAttributes in FMEAdmin settings)(can be used inFeature flagattribute basedtargeting rules)(called Metricdimension in FMEAdmin Settings)(appears underDimensionalAnalysis on MetricDetails page)(sent from SDK orintegration as eventproperty)Metric dimension/ Event propertyFeature flag metadata(Name, Traffic type, Owners,Tags, Description, Status)Segment metadata(Name, Traffic type,Owners, Tags, Description)Metric metadata(Name, Traffic type,Owners, Tags, Description,Metric category [Guardrailmetrics | None])Metric definitionAlert policy(Select desired impact[increase | decrease],Measure as [metrictype], Filter by, Cap at)Environment(allows separate control and data collection for staging, production, etc.)Change permissions Approval requiredData export permissions (Split Legacy) Any login user RestrictedEnvironment type Production Pre-productionAdmin API Key(Split Legacy)(if scoped toenvironment)(used with SplitPublic API)SDK API Key(always scoped toone environment)Feature flag definition(Treatments [variations],Targeting rules, Editingpermission overrides)Segment definition(list of keys or identifiers,Editing permissionoverrides)per Traffic type and Environment data pipelineData collectedImpressionsEvents(logs of rule evaluations, sent from SDKor Split Evaluator)(sent from SDK, API, or integrations)AttributionAlert notifications(associates events toFeature flag treatments[variations])Experiment(Name, Owners, Tags,Hypothesis, Assignment source[Feature flag,Environment], Scope [Start, End,Baseline treatment,Comparison treatments,Targeting rule], Keymetrics, Supportingmetrics) Any login user (Split Legacy) Restricted (Split Legacy)(always scoped toone environment)(always scoped toone environment)Metrics impact calculations(Key metrics, Supporting metrics) \ No newline at end of file From 3e99230171f4e3e3a8a6f96d280cb483d6df57a5 Mon Sep 17 00:00:00 2001 From: lena sano Date: Wed, 12 Mar 2025 10:13:53 -0300 Subject: [PATCH 17/19] remove outdated FME Key concepts page --- .../docs/key-concepts-updated.md | 124 ------------------ .../10-getting-started/docs/key-concepts.md | 16 ++- 2 files changed, 12 insertions(+), 128 deletions(-) delete mode 100644 docs/feature-management-experimentation/10-getting-started/docs/key-concepts-updated.md diff --git a/docs/feature-management-experimentation/10-getting-started/docs/key-concepts-updated.md b/docs/feature-management-experimentation/10-getting-started/docs/key-concepts-updated.md deleted file mode 100644 index 7be5a1a688e..00000000000 --- a/docs/feature-management-experimentation/10-getting-started/docs/key-concepts-updated.md +++ /dev/null @@ -1,124 +0,0 @@ ---- -title: Key concepts -sidebar_label: Key concepts (updated diagram) -sidebar_position: 3 -helpdocs_is_private: false -helpdocs_is_published: true ---- - -

- -

- -## Key concepts -Take 5 minutes to learn the foundational concepts of Harness Feature Management & Experimentation. - -## What is a feature flag? -A feature flag wraps or gates a section of your code, allowing it to be selectively turned on or off remotely with precision, down to the level of an individual user, at any time, without a new code deployment. - -### Decouple your deploy from your feature release -Feature flags allow you to decouple your deploy from your release, so your work in progress and new features are deployed in a turned-off state to any environment, which includes production, without impacting your users. - -### Control your release with targeting rules -Once your code is deployed, you can instantly turn on or off features for any individual user, group of users, or percentage of users, by creating or updating targeting rules. This approach facilitates faster software delivery practices with greater safety, including: - -* Trunk-based development to reduce time lost merging code branches -* Testing in production to allow dev, QA, and stakeholder review without impacting your users -* Early access or beta testing for a subset of your users in production -* Canary releases and monitored rollouts to limit the blast radius of release incidents -* Instant kill switches to shut off exposure to a feature without rollback or redeploy -* Infrastructure migration without downtime or risk of data loss -* Experimentation and A/B testing to make bigger bets with less risk - -## The role of data in Harness FME -FME provides visibility into your controlled releases by comparing data about feature flag evaluations with data about what happened after those evaluations. The data points that feed those comparisons are impressions and events. The results of those comparisons are called metrics. - -### Impressions -An impression is a record of a targeting decision made. It is created automatically each time a feature flag is evaluated and contains details about the user or unique key for which the evaluation was performed, the targeting decision, the targeting rule that drove that decision, and a time stamp. Refer to the [Impressions](https://help.split.io/hc/en-us/articles/360020585192-Impressions) guide for more information. - -### Events -An event is a record of user or system behavior. Events can be as simple as a page visited, a button clicked, or response time observed, and as complex as a transaction record with a detailed list of properties. An event doesn’t refer to a feature flag. The association between flag evaluations and events is computed for you. An event, associated with a user (or other unique keys), arriving after a flag decision for that same unique key, is attributed to that evaluation by FME’s attribution engine. - -To be ingested by FME, an event must contain the same user or unique key for which a feature flag evaluation was performed and a time stamp. Events are sent to FME from within your application, either from an existing customer data platform or error subsystem, or with a bulk upload using [Split Admin API](https://docs.split.io). Numerous events in integrations streamline event ingest for you. - -### Metrics -FME calculates metrics by attributing events to impressions and applying metric definitions to them. A metric definition can be as simple as a count of events per user or as complex as an average of values pulled from an event’s property after filtering those same events by another property. - -For example, from a stream of room_reservation events, calculate the average number of room nights booked for platinum members by examining the room_nights property after filtering the room_reservation events to those where the property club_membership = platinum. - -To promote one version of the truth, metrics are defined in a central location, not on a flag-by-flag basis, and all metrics are calculated for all flags. FME lets you elevate any metric your account created to be a key metric for a given feature flag. Then all the remaining metrics are sorted by impact and displayed immediately below the key metrics. This design, unique to FME, avoids blind spots caused by only looking for what you expect to find which automatically surfaces unexpected impacts. Refer to the [Metrics](https://help.split.io/hc/en-us/articles/22005565241101-Metrics) guide for more information. - -### Alerts -Alerts notify metric stakeholders and the team rolling out a particular feature when a metric threshold has exceeded a rollout or experiment that uses a percentage rollout rule. - -Alerts, like the metrics they are based on, are centrally defined once, and then applied to every rollout or experiment automatically. This is another design unique to FME. Our goal is to make learning and safety at speed the default experience, for every rollout. Once you define thresholds for metrics, any future rollout or experiment that exceeds them will fire an alert. When that happens, notifications are sent out, and an alert box is presented on the Targeting and Alerts tabs for the feature flag in question. Refer to the [Configuring metric alerting](https://help.split.io/hc/en-us/articles/19832312225293-Configuring-metric-alerting) guide for more information. - -## Using FME in your application -Targeting decisions are made locally, in memory, from within your own application code. There is never a reason to send private user data to Harness. Let’s take a look at how this is accomplished. - -### FME SDKs -To use Harness FME, include and initialize one of FME SDKs in your application. Once the SDK is initialized, targeting rules are retrieved from a nearby content delivery network (CDN) node, cached inside your code, and updated in real-time in milliseconds using a streaming architecture. - -As needed, your application makes a just-in-time call to the FME SDK in local memory, passing the feature flag name, the userId or unique key, and optionally, a map of user or session attributes. The response is returned instantly, with no need for a network call. After the evaluation is performed, the SDK asynchronously returns an impression record to Harness. Refer to our [SDK overview](https://help.split.io/hc/en-us/articles/360033557092-SDK-overview) for more information. - -### Split Evaluator -As an alternative to using FME SDKs, you can make REST API calls to a Split Evaluator hosted inside your own infrastructure. Like the SDK, this method never requires you to send private user data to the Harness network. The evaluator makes it possible to operate from within languages that do not yet have a published FME SDK and should only be used in that case. Refer to the [Split Evaluator](https://help.split.io/hc/en-us/articles/360020037072-Split-Evaluator) guide for more information. - -## FME's structure -Harness FME is architected to support teams and organizations of any size, from a single developer to multiple value-stream enterprises. Take a moment to familiarize yourself with the concepts of your Harness account, project, environment, and objects, e.g., users, user groups, tags, traffic types, feature flags, segments, and metrics. - -import FMEArchitectureObjectsImage from '@site/docs/feature-management-experimentation/10-getting-started/docs/FMEArchitectureObjectsImage.js'; - - - -:::info[Note: Split Legacy settings locations] -Post migration to app.harness.io, Split legacy Project permissions, Change permissions and Data export permissions (marked in purple above) will move out of their current locations and into Harness RBAC management. - -New Admin API Key creation and management will move to Harness Service Accounts. Existing Split legacy Admin API Keys will continue to operate until revoked in the Split legacy location. -::: - -### Account -Your company has one Harness account. Your account is the highest level container. Harness FME support may ask you for your account ID to speed troubleshooting. You’ll find your account ID in the URL for every page you visit in Harness. - -### Users -A Harness user is someone with access to the Harness user interface. Administrators can invite new users to Harness. All paid plans include SSO for user authentication and can support either invites or just in time provisioning. - -### User Groups -A group is a convenient way to manage a collection of users in your account. You can use groups to grant administrative controls and grant environment, feature flag, or segment-level controls. Refer to the [Manage user groups](https://help.split.io/hc/en-us/articles/360020812952-Manage-user-groups) guide for more information. - -### Projects -Projects provide separation or partitioning of work to reduce clutter or to enforce security. All accounts have at least one project. Use multiple projects only when you want to deliberately separate the work of different teams, product lines, or areas of work from each other. By design, objects within FME are not meant to be shared or moved across projects. Refer to the [Projects](https://help.split.io/hc/en-us/articles/360023534451-Workspaces) guide for more information. - -### Environment -Within each project, you may have multiple environments, such as development, staging, and production. Refer to the [Environments](https://help.split.io/hc/en-us/articles/360019915771-Environments) guide for more information. - -### Feature Flags -Feature flags are created at the project level where you specify the feature flag name, traffic type, owners, and description. Targeting rules are then created and managed at the environment level as part of the feature flag definition. Refer to the [Feature flag management](https://help.split.io/hc/en-us/articles/9650375859597-Feature-flag-management) guide for more information. - -### Targeting rule -Targeting rules for each feature flag are created at the environment level. For example, this supports one set of rules in your staging environment and another in production. Rules may be based on user or device attributes, membership in a segment, a percentage of a randomly distributed population, a list of individually specified user or unique key targets, or any combination of the above. - - - -### Segment -A segment is a list of users or unique keys for targeting purposes. Segments are created at the environment level. Refer to the [Segments](https://help.split.io/hc/en-us/articles/360020407512-Create-a-segment) guide for more information. - -### Traffic type -Targeting decisions are made on a per-user or per unique key basis, but what are the available types of unique keys you intend to target? These are your traffic types, and you can define up to ten unique key types at the project level. - -For feature flags that make decisions or observe metrics at the userId level, the traffic type should be user. If decisions and observations are based on account membership (to facilitate all users for a particular customer being treated the same, for instance), the traffic type should be account. Other common types are anonymous and device, but you have total flexibility in employing different traffic types. Refer to the [Traffic type](https://help.split.io/hc/en-us/articles/360019916311-Traffic-type) guide for more information. - -### Tag -Use tags to organize and filter feature flags, segments, and metrics across the Harness user interface. Because they allow you to filter items in lists, they are a great way to filter by team, epic, layer of system (front-end vs back-end), or any other. Refer to the [Tags](https://help.split.io/hc/en-us/articles/360020839151-Tags) guide for more information on how to use them. - -### Statuses -Statuses provide a way for teams to indicate which stage of a release or rollout a feature is in at any given moment, and as a way for teammates to filter their feature flags to see only features in a particular stage of the internal release process. There is a fixed list of status types. Refer to the [Use statuses](https://help.split.io/hc/en-us/articles/4405023981197-Use-statuses) guide for more information. - - \ No newline at end of file diff --git a/docs/feature-management-experimentation/10-getting-started/docs/key-concepts.md b/docs/feature-management-experimentation/10-getting-started/docs/key-concepts.md index 550573b9284..547e53a7e38 100644 --- a/docs/feature-management-experimentation/10-getting-started/docs/key-concepts.md +++ b/docs/feature-management-experimentation/10-getting-started/docs/key-concepts.md @@ -1,6 +1,6 @@ --- title: Key concepts -sidebar_label: Key concepts (copy & paste) +sidebar_label: Key concepts sidebar_position: 3 helpdocs_is_private: false helpdocs_is_published: true @@ -65,9 +65,17 @@ As needed, your application makes a just-in-time call to the FME SDK in local me As an alternative to using FME SDKs, you can make REST API calls to a Split Evaluator hosted inside your own infrastructure. Like the SDK, this method never requires you to send private user data to the Harness network. The evaluator makes it possible to operate from within languages that do not yet have a published FME SDK and should only be used in that case. Refer to the [Split Evaluator](https://help.split.io/hc/en-us/articles/360020037072-Split-Evaluator) guide for more information. ## FME's structure -Harness FME is architected to support teams and organizations of any size, from a single developer to multiple value-stream enterprises. Take a moment to familiarize yourself with the concepts of your Harness account, project, environment, and objects, e.g., users, groups, tags, traffic types, feature flags, segments, and metrics. +Harness FME is architected to support teams and organizations of any size, from a single developer to multiple value-stream enterprises. Take a moment to familiarize yourself with the concepts of your Harness account, project, environment, and objects, e.g., users, user groups, tags, traffic types, feature flags, segments, and metrics. -![](https://help.split.io/hc/article_attachments/30794709286029) +import FMEArchitectureObjectsImage from '@site/docs/feature-management-experimentation/10-getting-started/docs/FMEArchitectureObjectsImage.js'; + + + +:::info[Note: Split Legacy settings locations] +Post migration to app.harness.io, Split legacy Project permissions, Change permissions and Data export permissions (marked in purple above) will move out of their current locations and into Harness RBAC management. + +New Admin API Key creation and management will move to Harness Service Accounts. Existing Split legacy Admin API Keys will continue to operate until revoked in the Split legacy location. +::: ### Account Your company has one Harness account. Your account is the highest level container. Harness FME support may ask you for your account ID to speed troubleshooting. You’ll find your account ID in the URL for every page you visit in Harness. @@ -75,7 +83,7 @@ Your company has one Harness account. Your account is the highest level containe ### Users A Harness user is someone with access to the Harness user interface. Administrators can invite new users to Harness. All paid plans include SSO for user authentication and can support either invites or just in time provisioning. -### Groups +### User Groups A group is a convenient way to manage a collection of users in your account. You can use groups to grant administrative controls and grant environment, feature flag, or segment-level controls. Refer to the [Manage user groups](https://help.split.io/hc/en-us/articles/360020812952-Manage-user-groups) guide for more information. ### Projects From ef070681d5885c4ff2189d7ebc4315e096bc64c5 Mon Sep 17 00:00:00 2001 From: lena sano Date: Wed, 12 Mar 2025 11:16:13 -0300 Subject: [PATCH 18/19] FME structure image, minor realignments --- .../docs/static/fme-architecture-objects-dark.svg | 2 +- .../docs/static/fme-architecture-objects-light.svg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects-dark.svg b/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects-dark.svg index fb58122a206..edd4a8d0d57 100644 --- a/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects-dark.svg +++ b/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects-dark.svg @@ -1,2 +1,2 @@ Account(Account ID, Account name, Plan type) Admin API Key(Split Legacy)(if globally scoped)(used with SplitPublic API)User(a Harness login)Group(a list of Harnesslogins)Tag(created in the Tag inputfield of a Segment, Featureflag, or Metric)Project(no FME data or FME relationships are shared between projects)Project permissions (Split Legacy) Anyone can access Restrict who can accessRequire comments Require title and comments forfeature flag, segment,environment, and metric changes.Traffic type(classifies the type ofkey, or uniqueidentifier, for whichfeature flag targetingdecisions are madeand data is collected)(control access rights/view permissions)Flag set(created using SplitPublic API)(a list of Featureflags)(optimizes SDKinitialization bylimiting the numberof downloadedFeature flagdefinitions tospecific Flag set(s))Attribute /Traffic typeattribute(called Traffic typeAttributes in FMEAdmin settings)(can be used inFeature flagattribute basedtargeting rules)(called Metricdimension in FMEAdmin Settings)(appears underDimensionalAnalysis on MetricDetails page)(sent from SDK orintegration as eventproperty)Metric dimension/ Event propertyFeature flag metadata(Name, Traffic type, Owners,Tags, Description, Status)Segment metadata(Name, Traffic type,Owners, Tags, Description)Metric metadata(Name, Traffic type,Owners, Tags, Description,Metric category [Guardrailmetrics | None])Metric definitionAlert policy(Select desired impact[increase | decrease],Measure as [metrictype], Filter by, Cap at)Environment(allows separate control and data collection for staging, production, etc.)Change permissions Approval requiredData export permissions (Split Legacy) Any login user RestrictedEnvironment type Production Pre-productionAdmin API Key(Split Legacy)(if scoped toenvironment)(used with SplitPublic API)SDK API Key(always scoped toone environment)Feature flag definition(Treatments [variations],Targeting rules, Editingpermission overrides)Segment definition(list of keys or identifiers,Editing permissionoverrides)per Traffic type and Environment data pipelineData collectedImpressionsEvents(logs of rule evaluations, sent from SDKor Split Evaluator)(sent from SDK, API, or integrations)AttributionAlert notifications(associates events toFeature flag treatments[variations])Experiment(Name, Owners, Tags,Hypothesis, Assignment source[Feature flag,Environment], Scope [Start, End,Baseline treatment,Comparison treatments,Targeting rule], Keymetrics, Supportingmetrics) Any login user (Split Legacy) Restricted (Split Legacy)(always scoped toone environment)(always scoped toone environment)Metrics impact calculations(Key metrics, Supporting metrics) \ No newline at end of file + @font-face { font-family: Nunito; src: url(data:font/woff2;base64,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); }Account(Account ID, Account name, Plan type) Admin API Key(Split Legacy)(if globally scoped)(used with SplitPublic API)User(a Harness login)Group(a list of Harnesslogins)Tag(created in the Tag inputfield of a Segment, Featureflag, or Metric)Project(no FME data or FME relationships are shared between projects)Project permissions (Split Legacy) Anyone can access Restrict who can accessRequire comments Require title and comments forfeature flag, segment,environment, and metric changes.Traffic type(classifies the type ofkey, or uniqueidentifier, for whichfeature flag targetingdecisions are madeand data is collected)(control access rights/view permissions)Flag set(created using SplitPublic API)(a list of Featureflags)(optimizes SDKinitialization bylimiting the numberof downloadedFeature flagdefinitions tospecific Flag set(s))Attribute /Traffic typeattribute(called Traffic typeAttributes in FMEAdmin settings)(can be used inFeature flagattribute basedtargeting rules)(called Metricdimension in FMEAdmin Settings)(appears underDimensionalAnalysis on MetricDetails page)(sent from SDK orintegration as eventproperty)Metric dimension/ Event propertyFeature flag metadata(Name, Traffic type, Owners,Tags, Description, Status)Segment metadata(Name, Traffic type,Owners, Tags, Description)Metric metadata(Name, Traffic type,Owners, Tags, Description,Metric category [Guardrailmetrics | None])Metric definitionAlert policy(Select desired impact[increase | decrease],Measure as [metrictype], Filter by, Cap at)Environment(allows separate control and data collection for staging, production, etc.)Change permissions Approval requiredData export permissions (Split Legacy) Any login user RestrictedEnvironment type Production Pre-productionAdmin API Key(Split Legacy)(if scoped toenvironment)(used with SplitPublic API)SDK API Key(always scoped toone environment)Feature flag definition(Treatments [variations],Targeting rules, Editingpermission overrides)Segment definition(list of keys or identifiers,Editing permissionoverrides)per Traffic type and Environment data pipelineData collectedImpressionsEvents(logs of rule evaluations, sent from SDK or Split Evaluator)(sent from SDK, API, or integrations)AttributionAlert notifications(associates events toFeature flag treatments[variations])Experiment(Name, Owners, Tags,Hypothesis, Assignment source[Feature flag,Environment], Scope [Start, End,Baseline treatment,Comparison treatments,Targeting rule], Keymetrics, Supportingmetrics) Any login user (Split Legacy) Restricted (Split Legacy)(always scoped toone environment)(always scoped toone environment)Metrics impact calculations(Key metrics, Supporting metrics) \ No newline at end of file diff --git a/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects-light.svg b/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects-light.svg index 6eed0724818..fb2eb150774 100644 --- a/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects-light.svg +++ b/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects-light.svg @@ -1,2 +1,2 @@ Account(Account ID, Account name, Plan type) Admin API Key(Split Legacy)(if globally scoped)(used with SplitPublic API)User(a Harness login)Group(a list of Harnesslogins)Tag(created in the Tag inputfield of a Segment, Featureflag, or Metric)Project(no FME data or FME relationships are shared between projects)Project permissions (Split Legacy) Anyone can access Restrict who can accessRequire comments Require title and comments forfeature flag, segment,environment, and metric changes.Traffic type(classifies the type ofkey, or uniqueidentifier, for whichfeature flag targetingdecisions are madeand data is collected)(control access rights/view permissions)Flag set(created using SplitPublic API)(a list of Featureflags)(optimizes SDKinitialization bylimiting the numberof downloadedFeature flagdefinitions tospecific Flag set(s))Attribute /Traffic typeattribute(called Traffic typeAttributes in FMEAdmin settings)(can be used inFeature flagattribute basedtargeting rules)(called Metricdimension in FMEAdmin Settings)(appears underDimensionalAnalysis on MetricDetails page)(sent from SDK orintegration as eventproperty)Metric dimension/ Event propertyFeature flag metadata(Name, Traffic type, Owners,Tags, Description, Status)Segment metadata(Name, Traffic type,Owners, Tags, Description)Metric metadata(Name, Traffic type,Owners, Tags, Description,Metric category [Guardrailmetrics | None])Metric definitionAlert policy(Select desired impact[increase | decrease],Measure as [metrictype], Filter by, Cap at)Environment(allows separate control and data collection for staging, production, etc.)Change permissions Approval requiredData export permissions (Split Legacy) Any login user RestrictedEnvironment type Production Pre-productionAdmin API Key(Split Legacy)(if scoped toenvironment)(used with SplitPublic API)SDK API Key(always scoped toone environment)Feature flag definition(Treatments [variations],Targeting rules, Editingpermission overrides)Segment definition(list of keys or identifiers,Editing permissionoverrides)per Traffic type and Environment data pipelineData collectedImpressionsEvents(logs of rule evaluations, sent from SDKor Split Evaluator)(sent from SDK, API, or integrations)AttributionAlert notifications(associates events toFeature flag treatments[variations])Experiment(Name, Owners, Tags,Hypothesis, Assignment source[Feature flag,Environment], Scope [Start, End,Baseline treatment,Comparison treatments,Targeting rule], Keymetrics, Supportingmetrics) Any login user (Split Legacy) Restricted (Split Legacy)(always scoped toone environment)(always scoped toone environment)Metrics impact calculations(Key metrics, Supporting metrics) \ No newline at end of file + @font-face { font-family: Nunito; src: url(data:font/woff2;base64,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); }Account(Account ID, Account name, Plan type) Admin API Key(Split Legacy)(if globally scoped)(used with SplitPublic API)User(a Harness login)Group(a list of Harnesslogins)Tag(created in the Tag inputfield of a Segment, Featureflag, or Metric)Project(no FME data or FME relationships are shared between projects)Project permissions (Split Legacy) Anyone can access Restrict who can accessRequire comments Require title and comments forfeature flag, segment,environment, and metric changes.Traffic type(classifies the type ofkey, or uniqueidentifier, for whichfeature flag targetingdecisions are madeand data is collected)(control access rights/view permissions)Flag set(created using SplitPublic API)(a list of Featureflags)(optimizes SDKinitialization bylimiting the numberof downloadedFeature flagdefinitions tospecific Flag set(s))Attribute /Traffic typeattribute(called Traffic typeAttributes in FMEAdmin settings)(can be used inFeature flagattribute basedtargeting rules)(called Metricdimension in FMEAdmin Settings)(appears underDimensionalAnalysis on MetricDetails page)(sent from SDK orintegration as eventproperty)Metric dimension/ Event propertyFeature flag metadata(Name, Traffic type, Owners,Tags, Description, Status)Segment metadata(Name, Traffic type,Owners, Tags, Description)Metric metadata(Name, Traffic type,Owners, Tags, Description,Metric category [Guardrailmetrics | None])Metric definitionAlert policy(Select desired impact[increase | decrease],Measure as [metrictype], Filter by, Cap at)Environment(allows separate control and data collection for staging, production, etc.)Change permissions Approval requiredData export permissions (Split Legacy) Any login user RestrictedEnvironment type Production Pre-productionAdmin API Key(Split Legacy)(if scoped toenvironment)(used with SplitPublic API)SDK API Key(always scoped toone environment)Feature flag definition(Treatments [variations],Targeting rules, Editingpermission overrides)Segment definition(list of keys or identifiers,Editing permissionoverrides)per Traffic type and Environment data pipelineData collectedImpressionsEvents(logs of rule evaluations, sent from SDK or Split Evaluator)(sent from SDK, API, or integrations)AttributionAlert notifications(associates events toFeature flag treatments[variations])Experiment(Name, Owners, Tags,Hypothesis, Assignment source[Feature flag,Environment], Scope [Start, End,Baseline treatment,Comparison treatments,Targeting rule], Keymetrics, Supportingmetrics) Any login user (Split Legacy) Restricted (Split Legacy)(always scoped toone environment)(always scoped toone environment)Metrics impact calculations(Key metrics, Supporting metrics) \ No newline at end of file From 74e09610dfcf4647beef47db31ac6380789a41d4 Mon Sep 17 00:00:00 2001 From: lena sano Date: Wed, 12 Mar 2025 11:38:17 -0300 Subject: [PATCH 19/19] FME structure image - align object property names with UI --- .../static/fme-architecture-objects-dark.svg | 2 +- .../static/fme-architecture-objects-light.svg | 2 +- .../fme-architecture-objects.excalidraw | 1004 +++++++++-------- 3 files changed, 520 insertions(+), 488 deletions(-) diff --git a/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects-dark.svg b/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects-dark.svg index edd4a8d0d57..7a007079e98 100644 --- a/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects-dark.svg +++ b/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects-dark.svg @@ -1,2 +1,2 @@ Account(Account ID, Account name, Plan type) Admin API Key(Split Legacy)(if globally scoped)(used with SplitPublic API)User(a Harness login)Group(a list of Harnesslogins)Tag(created in the Tag inputfield of a Segment, Featureflag, or Metric)Project(no FME data or FME relationships are shared between projects)Project permissions (Split Legacy) Anyone can access Restrict who can accessRequire comments Require title and comments forfeature flag, segment,environment, and metric changes.Traffic type(classifies the type ofkey, or uniqueidentifier, for whichfeature flag targetingdecisions are madeand data is collected)(control access rights/view permissions)Flag set(created using SplitPublic API)(a list of Featureflags)(optimizes SDKinitialization bylimiting the numberof downloadedFeature flagdefinitions tospecific Flag set(s))Attribute /Traffic typeattribute(called Traffic typeAttributes in FMEAdmin settings)(can be used inFeature flagattribute basedtargeting rules)(called Metricdimension in FMEAdmin Settings)(appears underDimensionalAnalysis on MetricDetails page)(sent from SDK orintegration as eventproperty)Metric dimension/ Event propertyFeature flag metadata(Name, Traffic type, Owners,Tags, Description, Status)Segment metadata(Name, Traffic type,Owners, Tags, Description)Metric metadata(Name, Traffic type,Owners, Tags, Description,Metric category [Guardrailmetrics | None])Metric definitionAlert policy(Select desired impact[increase | decrease],Measure as [metrictype], Filter by, Cap at)Environment(allows separate control and data collection for staging, production, etc.)Change permissions Approval requiredData export permissions (Split Legacy) Any login user RestrictedEnvironment type Production Pre-productionAdmin API Key(Split Legacy)(if scoped toenvironment)(used with SplitPublic API)SDK API Key(always scoped toone environment)Feature flag definition(Treatments [variations],Targeting rules, Editingpermission overrides)Segment definition(list of keys or identifiers,Editing permissionoverrides)per Traffic type and Environment data pipelineData collectedImpressionsEvents(logs of rule evaluations, sent from SDK or Split Evaluator)(sent from SDK, API, or integrations)AttributionAlert notifications(associates events toFeature flag treatments[variations])Experiment(Name, Owners, Tags,Hypothesis, Assignment source[Feature flag,Environment], Scope [Start, End,Baseline treatment,Comparison treatments,Targeting rule], Keymetrics, Supportingmetrics) Any login user (Split Legacy) Restricted (Split Legacy)(always scoped toone environment)(always scoped toone environment)Metrics impact calculations(Key metrics, Supporting metrics) \ No newline at end of file + @font-face { font-family: Nunito; src: url(data:font/woff2;base64,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); }Account(Account ID, Account name, Plan type) Admin API Key(Split Legacy)(if globally scoped)(used with SplitPublic API)User(a Harness login)Group(a list of Harnesslogins)Tag(created in the Tag inputfield of a Segment, Featureflag, or Metric)Project(no FME data or FME relationships are shared between projects)Project permissions (Split Legacy) Anyone can access Restrict who can accessRequire comments Require title and comments forfeature flag, segment,environment, and metric changes.Traffic type(classifies the type ofkey, or uniqueidentifier, for whichfeature flag targetingdecisions are madeand data is collected)(control access rights/view permissions)Flag set(created using SplitPublic API)(a list of Featureflags)(optimizes SDKinitialization bylimiting the numberof downloadedFeature flagdefinitions tospecific Flag set(s))Attribute /Traffic typeattribute(called Traffic typeAttributes in FMEAdmin settings)(can be used inFeature flagattribute basedtargeting rules)(called Metricdimension in FMEAdmin Settings)(appears underDimensionalAnalysis on MetricDetails page)(sent from SDK orintegration as eventproperty)Metric dimension/ Event propertyFeature flag metadata(Name, Traffic type, Description,Rollout status, Tags, Owners)Segment metadata(Name, Segment type[Regular|Large], Traffic type,Owners, Tags, Description)Metric metadata(Name, Owners, Tags,Description, Metric category[Guardrail metrics | None])Metric definitionAlert policy(Select desired impact[Increase|Decrease],Select traffic type,Measure as [metric type],Filter by, Cap at)Environment(allows separate control and data collection for staging, production, etc.)Change permissions Approval requiredData export permissions (Split Legacy) Any login user RestrictedEnvironment type Production Pre-productionAdmin API Key(Split Legacy)(if scoped toenvironment)(used with SplitPublic API)SDK API Key(always scoped toone environment)Feature flag definition(Treatments [variations],Targeting rules, Editingpermission overrides)Segment definition(list of keys or identifiers,Editing permissionoverrides)per Traffic type and Environment data pipelineData collectedImpressionsEvents(logs of rule evaluations, sent from SDK or Split Evaluator)(sent from SDK, API, or integrations)AttributionAlert notifications(associates events toFeature flag treatments[variations])Experiment(Name, Owners, Tags,Hypothesis, Assignment source[Feature flag,Environment], Scope [Start, End,Baseline treatment,Comparison treatments,Targeting rule], Keymetrics, Supportingmetrics) Any login user (Split Legacy) Restricted (Split Legacy)(always scoped toone environment)(always scoped toone environment)Metrics impact calculations(Key metrics, Supporting metrics) \ No newline at end of file diff --git a/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects-light.svg b/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects-light.svg index fb2eb150774..b8c783676d3 100644 --- a/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects-light.svg +++ b/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects-light.svg @@ -1,2 +1,2 @@ Account(Account ID, Account name, Plan type) Admin API Key(Split Legacy)(if globally scoped)(used with SplitPublic API)User(a Harness login)Group(a list of Harnesslogins)Tag(created in the Tag inputfield of a Segment, Featureflag, or Metric)Project(no FME data or FME relationships are shared between projects)Project permissions (Split Legacy) Anyone can access Restrict who can accessRequire comments Require title and comments forfeature flag, segment,environment, and metric changes.Traffic type(classifies the type ofkey, or uniqueidentifier, for whichfeature flag targetingdecisions are madeand data is collected)(control access rights/view permissions)Flag set(created using SplitPublic API)(a list of Featureflags)(optimizes SDKinitialization bylimiting the numberof downloadedFeature flagdefinitions tospecific Flag set(s))Attribute /Traffic typeattribute(called Traffic typeAttributes in FMEAdmin settings)(can be used inFeature flagattribute basedtargeting rules)(called Metricdimension in FMEAdmin Settings)(appears underDimensionalAnalysis on MetricDetails page)(sent from SDK orintegration as eventproperty)Metric dimension/ Event propertyFeature flag metadata(Name, Traffic type, Owners,Tags, Description, Status)Segment metadata(Name, Traffic type,Owners, Tags, Description)Metric metadata(Name, Traffic type,Owners, Tags, Description,Metric category [Guardrailmetrics | None])Metric definitionAlert policy(Select desired impact[increase | decrease],Measure as [metrictype], Filter by, Cap at)Environment(allows separate control and data collection for staging, production, etc.)Change permissions Approval requiredData export permissions (Split Legacy) Any login user RestrictedEnvironment type Production Pre-productionAdmin API Key(Split Legacy)(if scoped toenvironment)(used with SplitPublic API)SDK API Key(always scoped toone environment)Feature flag definition(Treatments [variations],Targeting rules, Editingpermission overrides)Segment definition(list of keys or identifiers,Editing permissionoverrides)per Traffic type and Environment data pipelineData collectedImpressionsEvents(logs of rule evaluations, sent from SDK or Split Evaluator)(sent from SDK, API, or integrations)AttributionAlert notifications(associates events toFeature flag treatments[variations])Experiment(Name, Owners, Tags,Hypothesis, Assignment source[Feature flag,Environment], Scope [Start, End,Baseline treatment,Comparison treatments,Targeting rule], Keymetrics, Supportingmetrics) Any login user (Split Legacy) Restricted (Split Legacy)(always scoped toone environment)(always scoped toone environment)Metrics impact calculations(Key metrics, Supporting metrics) \ No newline at end of file + @font-face { font-family: Nunito; src: url(data:font/woff2;base64,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); }Account(Account ID, Account name, Plan type) Admin API Key(Split Legacy)(if globally scoped)(used with SplitPublic API)User(a Harness login)Group(a list of Harnesslogins)Tag(created in the Tag inputfield of a Segment, Featureflag, or Metric)Project(no FME data or FME relationships are shared between projects)Project permissions (Split Legacy) Anyone can access Restrict who can accessRequire comments Require title and comments forfeature flag, segment,environment, and metric changes.Traffic type(classifies the type ofkey, or uniqueidentifier, for whichfeature flag targetingdecisions are madeand data is collected)(control access rights/view permissions)Flag set(created using SplitPublic API)(a list of Featureflags)(optimizes SDKinitialization bylimiting the numberof downloadedFeature flagdefinitions tospecific Flag set(s))Attribute /Traffic typeattribute(called Traffic typeAttributes in FMEAdmin settings)(can be used inFeature flagattribute basedtargeting rules)(called Metricdimension in FMEAdmin Settings)(appears underDimensionalAnalysis on MetricDetails page)(sent from SDK orintegration as eventproperty)Metric dimension/ Event propertyFeature flag metadata(Name, Traffic type, Description,Rollout status, Tags, Owners)Segment metadata(Name, Segment type[Regular|Large], Traffic type,Owners, Tags, Description)Metric metadata(Name, Owners, Tags,Description, Metric category[Guardrail metrics | None])Metric definitionAlert policy(Select desired impact[Increase|Decrease],Select traffic type,Measure as [metric type],Filter by, Cap at)Environment(allows separate control and data collection for staging, production, etc.)Change permissions Approval requiredData export permissions (Split Legacy) Any login user RestrictedEnvironment type Production Pre-productionAdmin API Key(Split Legacy)(if scoped toenvironment)(used with SplitPublic API)SDK API Key(always scoped toone environment)Feature flag definition(Treatments [variations],Targeting rules, Editingpermission overrides)Segment definition(list of keys or identifiers,Editing permissionoverrides)per Traffic type and Environment data pipelineData collectedImpressionsEvents(logs of rule evaluations, sent from SDK or Split Evaluator)(sent from SDK, API, or integrations)AttributionAlert notifications(associates events toFeature flag treatments[variations])Experiment(Name, Owners, Tags,Hypothesis, Assignment source[Feature flag,Environment], Scope [Start, End,Baseline treatment,Comparison treatments,Targeting rule], Keymetrics, Supportingmetrics) Any login user (Split Legacy) Restricted (Split Legacy)(always scoped toone environment)(always scoped toone environment)Metrics impact calculations(Key metrics, Supporting metrics) \ No newline at end of file diff --git a/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects.excalidraw b/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects.excalidraw index a2789b62604..4aee5ceb314 100644 --- a/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects.excalidraw +++ b/docs/feature-management-experimentation/10-getting-started/docs/static/fme-architecture-objects.excalidraw @@ -605,11 +605,11 @@ "index": "aP", "roundness": null, "seed": 1290926918, - "version": 692, - "versionNonce": 302629401, + "version": 716, + "versionNonce": 550470201, "isDeleted": false, "boundElements": [], - "updated": 1739348706708, + "updated": 1741782672390, "link": null, "locked": false, "text": "(a list of Harness\nlogins)", @@ -783,8 +783,8 @@ { "id": "c-rIipPmkQidKpM37P8jw", "type": "rectangle", - "x": 411.0751850360199, - "y": 41.19901273819499, + "x": 414.3007705672245, + "y": 41.522599035327566, "width": 1174.5274637028388, "height": 1496.0284650073986, "angle": 0, @@ -800,11 +800,11 @@ "index": "b4a", "roundness": null, "seed": 1485459142, - "version": 1879, - "versionNonce": 1991012057, + "version": 1885, + "versionNonce": 2095966775, "isDeleted": false, "boundElements": [], - "updated": 1739350305607, + "updated": 1741782637851, "link": null, "locked": false }, @@ -1354,11 +1354,11 @@ "index": "b4t", "roundness": null, "seed": 256948742, - "version": 2673, - "versionNonce": 1028328730, + "version": 2701, + "versionNonce": 1159088151, "isDeleted": false, "boundElements": [], - "updated": 1739279605636, + "updated": 1741781379282, "link": null, "locked": false }, @@ -1382,11 +1382,11 @@ "index": "b4u", "roundness": null, "seed": 1517783366, - "version": 2908, - "versionNonce": 1969117658, + "version": 2932, + "versionNonce": 1183683031, "isDeleted": false, "boundElements": [], - "updated": 1739279605636, + "updated": 1741781377451, "link": null, "locked": false }, @@ -1410,11 +1410,11 @@ "index": "b4w", "roundness": null, "seed": 243429510, - "version": 2555, - "versionNonce": 250220186, + "version": 2588, + "versionNonce": 1872251799, "isDeleted": false, "boundElements": [], - "updated": 1739279605636, + "updated": 1741781373730, "link": null, "locked": false }, @@ -1460,7 +1460,7 @@ "type": "text", "x": 795.0948557966076, "y": 357.98415399712036, - "width": 132.03518612198633, + "width": 123.80171593613028, "height": 201.79815899960101, "angle": 0, "strokeColor": "#1e1e1e", @@ -1475,14 +1475,14 @@ "index": "b4z", "roundness": null, "seed": 546866950, - "version": 3410, - "versionNonce": 1792454682, + "version": 3432, + "versionNonce": 1024559801, "isDeleted": false, "boundElements": [], - "updated": 1739279605636, + "updated": 1741781401082, "link": null, "locked": false, - "text": "(created using Split\nPublic API)\n(a list of Feature\nflags)\n(optimizes SDK\ninitialization by\nlimiting the number\nof downloaded\nFeature flag\ndefinitions to specific\nFlag set(s))", + "text": "(created using Split\nPublic API)\n(a list of Feature\nflags)\n(optimizes SDK\ninitialization by\nlimiting the number\nof downloaded\nFeature flag\ndefinitions to\nspecific Flag set(s))", "fontSize": 13.589101616134748, "fontFamily": 6, "textAlign": "left", @@ -1904,10 +1904,10 @@ { "id": "0S13Y0QlI5473m9kTAIoI", "type": "text", - "x": 650.028515433051, - "y": 252.60491575617598, - "width": 191.82507695867696, - "height": 38.428947931699845, + "x": 647.0652316217881, + "y": 253.77042687460076, + "width": 209.79930611583362, + "height": 37.2634368132751, "angle": 0, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", @@ -1921,20 +1921,20 @@ "index": "b5F", "roundness": null, "seed": 1313711238, - "version": 1630, - "versionNonce": 917281434, + "version": 1749, + "versionNonce": 332175287, "isDeleted": false, "boundElements": [], - "updated": 1739279605636, + "updated": 1741789590049, "link": null, "locked": false, - "text": "(Name, Traffic type, Owners,\nTags, Description)", - "fontSize": 14.23294367840735, + "text": "(Name, Traffic type, Description,\nRollout status, Tags, Owners)", + "fontSize": 13.801272893805592, "fontFamily": 6, "textAlign": "left", "verticalAlign": "top", "containerId": null, - "originalText": "(Name, Traffic type, Owners, Tags, Description)", + "originalText": "(Name, Traffic type, Description, Rollout status, Tags, Owners)", "autoResize": false, "lineHeight": 1.35 }, @@ -2062,10 +2062,10 @@ { "id": "s5dWjiO7niu4GCU4zuw-S", "type": "text", - "x": 890.0136249855219, - "y": 251.89084718745423, - "width": 175.90089593021492, - "height": 38.451356384121034, + "x": 891.2595000785959, + "y": 244.67380640108115, + "width": 176.9356232316952, + "height": 47.32814274482018, "angle": 0, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", @@ -2079,20 +2079,20 @@ "index": "b5K", "roundness": null, "seed": 710414534, - "version": 1612, - "versionNonce": 1857469018, + "version": 1918, + "versionNonce": 579320217, "isDeleted": false, "boundElements": [], - "updated": 1739279605636, + "updated": 1741789972801, "link": null, "locked": false, - "text": "(Name, Traffic type,\nOwners, Tags, Description)", - "fontSize": 14.241243105230009, + "text": "(Name, Segment type\n[Regular|Large], Traffic type,\nOwners, Tags, Description)", + "fontSize": 11.685961171560537, "fontFamily": 6, "textAlign": "left", "verticalAlign": "top", "containerId": null, - "originalText": "(Name, Traffic type, Owners, Tags, Description)", + "originalText": "(Name, Segment type [Regular|Large], Traffic type, Owners, Tags, Description)", "autoResize": false, "lineHeight": 1.35 }, @@ -2156,7 +2156,7 @@ "id": "arJu28qbib5ZrxZgzXvBV", "type": "rectangle", "x": 1115.2046718844315, - "y": 210.11472632021093, + "y": 210.2423110854876, "width": 219.93084502230602, "height": 371.7506319548874, "angle": 0, @@ -2172,19 +2172,19 @@ "index": "b5N", "roundness": null, "seed": 144457350, - "version": 2143, - "versionNonce": 1557455002, + "version": 2144, + "versionNonce": 1257227927, "isDeleted": false, "boundElements": [], - "updated": 1739279605636, + "updated": 1741780887339, "link": null, "locked": false }, { "id": "iZ8v3DPkAdncN1r8VwmRe", "type": "rectangle", - "x": 1136.1991613277514, - "y": 491.1551125071233, + "x": 1139.1991613277514, + "y": 499.1551125071233, "width": 184.1027013454992, "height": 68.6436359906346, "angle": 0, @@ -2200,19 +2200,19 @@ "index": "b5O", "roundness": null, "seed": 417234374, - "version": 1603, - "versionNonce": 996261210, + "version": 1614, + "versionNonce": 1827504665, "isDeleted": false, "boundElements": [], - "updated": 1739279605636, + "updated": 1741781233017, "link": null, "locked": false }, { "id": "Qx_HmD9Erg0Xy6aivPntX", "type": "rectangle", - "x": 1130.3593175777517, - "y": 484.64571680624675, + "x": 1133.3593175777517, + "y": 492.64571680624675, "width": 182.4935619559986, "height": 68.60870885965744, "angle": 0, @@ -2228,11 +2228,11 @@ "index": "b5P", "roundness": null, "seed": 1024906502, - "version": 1544, - "versionNonce": 364184090, + "version": 1555, + "versionNonce": 1144391671, "isDeleted": false, "boundElements": [], - "updated": 1739279605636, + "updated": 1741781233017, "link": null, "locked": false }, @@ -2278,8 +2278,8 @@ "type": "text", "x": 1127.9221449053339, "y": 251.90090780574184, - "width": 135.63023039193214, - "height": 58.2566975800916, + "width": 183.48302302171044, + "height": 58.25669758009159, "angle": 0, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", @@ -2293,28 +2293,28 @@ "index": "b5R", "roundness": null, "seed": 800967558, - "version": 2533, - "versionNonce": 320521114, + "version": 2736, + "versionNonce": 696472375, "isDeleted": false, "boundElements": [], - "updated": 1739279605636, + "updated": 1741789806101, "link": null, "locked": false, - "text": "(Name, Traffic type,\nOwners, Tags,\nDescription)", + "text": "(Name, Owners, Tags,\nDescription, Metric category\n[Guardrail metrics | None])", "fontSize": 14.384369772862119, "fontFamily": 6, "textAlign": "left", "verticalAlign": "top", "containerId": null, - "originalText": "(Name, Traffic type, Owners, Tags, Description)", + "originalText": "(Name, Owners, Tags, Description, Metric category [Guardrail metrics | None])", "autoResize": false, "lineHeight": 1.35 }, { "id": "9A1cSyigAeaWfUo-_vqVH", "type": "rectangle", - "x": 1127.3954142763612, - "y": 331.8076898348738, + "x": 1130.5229990416378, + "y": 339.8076898348738, "width": 178.31721877556808, "height": 133.83114621280322, "angle": 0, @@ -2330,19 +2330,19 @@ "index": "b5S", "roundness": null, "seed": 1581673158, - "version": 2578, - "versionNonce": 351486042, + "version": 2590, + "versionNonce": 1208473337, "isDeleted": false, "boundElements": [], - "updated": 1739279605636, + "updated": 1741781233017, "link": null, "locked": false }, { "id": "jQHlMDWIzCd3GoMARNKlY", "type": "text", - "x": 1137.82191564937, - "y": 339.22910290800615, + "x": 1140.9495004146468, + "y": 347.22910290800615, "width": 130.30189718031852, "height": 23.13672888360233, "angle": 0, @@ -2358,11 +2358,11 @@ "index": "b5T", "roundness": null, "seed": 1224286726, - "version": 1862, - "versionNonce": 1746806042, + "version": 1874, + "versionNonce": 2118119703, "isDeleted": false, "boundElements": [], - "updated": 1739279605636, + "updated": 1741781233017, "link": null, "locked": false, "text": "Metric definition", @@ -2378,8 +2378,8 @@ { "id": "L7FgmsexjsX6akQ_HRF2g", "type": "rectangle", - "x": 1125.5919187722895, - "y": 481.74689851713475, + "x": 1128.5919187722895, + "y": 489.74689851713475, "width": 180.01395417075696, "height": 65.80755935763469, "angle": 0, @@ -2395,19 +2395,19 @@ "index": "b5U", "roundness": null, "seed": 315814214, - "version": 2542, - "versionNonce": 101609946, + "version": 2553, + "versionNonce": 2059702233, "isDeleted": false, "boundElements": [], - "updated": 1739279605636, + "updated": 1741781233017, "link": null, "locked": false }, { "id": "9z95CWWEHVtU0qWaLQWwe", "type": "text", - "x": 1137.4306846298334, - "y": 491.6370963419029, + "x": 1140.4306846298334, + "y": 499.6370963419029, "width": 154.48103945591106, "height": 21.6, "angle": 0, @@ -2423,11 +2423,11 @@ "index": "b5V", "roundness": null, "seed": 1999689862, - "version": 1881, - "versionNonce": 1104619162, + "version": 1892, + "versionNonce": 1836037687, "isDeleted": false, "boundElements": [], - "updated": 1739279605636, + "updated": 1741781233017, "link": null, "locked": false, "text": "Alert policy", @@ -2443,10 +2443,10 @@ { "id": "ANIj0tJHBU7BxXOXPv6Qv", "type": "text", - "x": 1136.103680819423, - "y": 367.4825286921832, - "width": 163.84178325823396, - "height": 84.85052111618968, + "x": 1137.2270127591905, + "y": 372.4866854677207, + "width": 168.01120892107673, + "height": 97.05601405164218, "angle": 0, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", @@ -2460,20 +2460,20 @@ "index": "b5W", "roundness": null, "seed": 1037031366, - "version": 2748, - "versionNonce": 338379610, + "version": 3181, + "versionNonce": 329569913, "isDeleted": false, "boundElements": [], - "updated": 1739279605636, + "updated": 1741790000997, "link": null, "locked": false, - "text": "(Select desired impact\n[increase|decrease],\nMeasure as [metric\ntype], Filter by, Cap at)", - "fontSize": 15.71305946596105, + "text": "(Select desired impact\n[Increase|Decrease],\nSelect traffic type,\nMeasure as [metric type],\nFilter by, Cap at)", + "fontSize": 14.378668748391433, "fontFamily": 6, "textAlign": "left", "verticalAlign": "top", "containerId": null, - "originalText": "(Select desired impact [increase|decrease], Measure as [metric type], Filter by, Cap at)", + "originalText": "(Select desired impact [Increase|Decrease], Select traffic type, Measure as [metric type], Filter by, Cap at)", "autoResize": false, "lineHeight": 1.35 }, @@ -2649,7 +2649,7 @@ "id": "YzMxGTIWBvIWMQO1O9Jox", "type": "rectangle", "x": 463.0245327323861, - "y": 620.022084779684, + "y": 620.4090919010233, "width": 1045.0007435505383, "height": 861.3871247540291, "angle": 0, @@ -2665,11 +2665,11 @@ "index": "b5h", "roundness": null, "seed": 1946907270, - "version": 2597, - "versionNonce": 1211820761, + "version": 2635, + "versionNonce": 458855801, "isDeleted": false, "boundElements": [], - "updated": 1739350277656, + "updated": 1741781802100, "link": null, "locked": false }, @@ -3428,7 +3428,7 @@ "x": 911.1443473655324, "y": 888.6560837207894, "width": 207.32013061536216, - "height": 108, + "height": 64.80000000000001, "angle": 0, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", @@ -3442,20 +3442,20 @@ "index": "b68", "roundness": null, "seed": 1144995974, - "version": 1947, - "versionNonce": 1440692313, + "version": 1950, + "versionNonce": 2031650999, "isDeleted": false, "boundElements": [], - "updated": 1739350176546, + "updated": 1741779657215, "link": null, "locked": false, - "text": "(Treatments [variations],\nTargeting rules, Status,\nEditing permission\noverrides, Key metrics,\nSupporting metrics)", + "text": "(Treatments [variations],\nTargeting rules, Editing\npermission overrides)", "fontSize": 16, "fontFamily": 6, "textAlign": "left", "verticalAlign": "top", "containerId": null, - "originalText": "(Treatments [variations], Targeting rules, Status, Editing permission overrides, Key metrics, Supporting metrics)", + "originalText": "(Treatments [variations], Targeting rules, Editing permission overrides)", "autoResize": false, "lineHeight": 1.35 }, @@ -3479,11 +3479,11 @@ "index": "b69", "roundness": null, "seed": 751910854, - "version": 1885, - "versionNonce": 1687787607, + "version": 1932, + "versionNonce": 992134553, "isDeleted": false, "boundElements": [], - "updated": 1739350200249, + "updated": 1741781821115, "link": null, "locked": false }, @@ -3529,7 +3529,7 @@ "type": "rectangle", "x": 1210.7732543918755, "y": 892.1050589714669, - "width": 38.19755729821463, + "width": 33.69806790945655, "height": 18.46095262607082, "angle": 0, "strokeColor": "transparent", @@ -3544,21 +3544,21 @@ "index": "b6B", "roundness": null, "seed": 1651279430, - "version": 645, - "versionNonce": 1740404791, + "version": 693, + "versionNonce": 1565894041, "isDeleted": false, "boundElements": [], - "updated": 1739350213060, + "updated": 1741781844653, "link": null, "locked": false }, { "id": "HW9WWilpKbeVaFoVDO8Sc", "type": "text", - "x": 1163.0360427980643, - "y": 890.4026110707593, + "x": 1160.990433728128, + "y": 889.8412381035417, "width": 203.08792229074453, - "height": 86.4, + "height": 64.80000000000001, "angle": 0, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", @@ -3572,20 +3572,20 @@ "index": "b6C", "roundness": null, "seed": 684707206, - "version": 2733, - "versionNonce": 907581913, + "version": 3083, + "versionNonce": 1710501751, "isDeleted": false, "boundElements": [], - "updated": 1739350196528, + "updated": 1741781848270, "link": null, "locked": false, - "text": "(list of keys, or identifiers\nfor end users of your app,\nEditing permission\noverrides)", + "text": "(list of keys or identifiers,\nEditing permission\noverrides)", "fontSize": 16, "fontFamily": 6, "textAlign": "left", "verticalAlign": "top", "containerId": null, - "originalText": "(list of keys, or identifiers for end users of your app, Editing permission overrides)", + "originalText": "(list of keys or identifiers, Editing permission overrides)", "autoResize": false, "lineHeight": 1.35 }, @@ -3597,7 +3597,7 @@ "width": 948.3525569044615, "height": 373.29657602364614, "angle": 0, - "strokeColor": "#868e96", + "strokeColor": "#1e1e1e", "backgroundColor": "#e9ecef", "fillStyle": "solid", "strokeWidth": 4, @@ -3609,11 +3609,11 @@ "index": "b6r", "roundness": null, "seed": 700984518, - "version": 1249, - "versionNonce": 316101177, + "version": 1250, + "versionNonce": 1202854167, "isDeleted": false, "boundElements": [], - "updated": 1739350261695, + "updated": 1741779938420, "link": null, "locked": false }, @@ -3625,7 +3625,7 @@ "width": 947.7348690927762, "height": 373.29657602364614, "angle": 0, - "strokeColor": "#868e96", + "strokeColor": "#1e1e1e", "backgroundColor": "#e9ecef", "fillStyle": "solid", "strokeWidth": 4, @@ -3637,23 +3637,23 @@ "index": "b7W", "roundness": null, "seed": 49456134, - "version": 1433, - "versionNonce": 648595927, + "version": 1434, + "versionNonce": 1458104281, "isDeleted": false, "boundElements": [], - "updated": 1739350261695, + "updated": 1741779938420, "link": null, "locked": false }, { "id": "qjX9vTW2n3tOddUIEaigj", "type": "rectangle", - "x": 505.5626605178058, - "y": 1050.267710932084, + "x": 505.69024528308256, + "y": 1050.4690094298237, "width": 949.0435951437843, "height": 373.29657602364614, "angle": 0, - "strokeColor": "#868e96", + "strokeColor": "#1e1e1e", "backgroundColor": "#e9ecef", "fillStyle": "solid", "strokeWidth": 4, @@ -3665,11 +3665,11 @@ "index": "b7X", "roundness": null, "seed": 374092614, - "version": 844, - "versionNonce": 209888025, + "version": 847, + "versionNonce": 1561857273, "isDeleted": false, "boundElements": [], - "updated": 1739350261695, + "updated": 1741788613671, "link": null, "locked": false }, @@ -3681,7 +3681,7 @@ "width": 945.562216886453, "height": 1.5167397596521823, "angle": 0, - "strokeColor": "#868e96", + "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 4, @@ -3693,11 +3693,11 @@ "index": "b7Z", "roundness": null, "seed": 2117196422, - "version": 2677, - "versionNonce": 1824487159, + "version": 2678, + "versionNonce": 2083675225, "isDeleted": false, "boundElements": [], - "updated": 1739350261695, + "updated": 1741779929636, "link": null, "locked": false, "points": [ @@ -3780,7 +3780,7 @@ "width": 334.1277778148651, "height": 21.6, "angle": 0, - "strokeColor": "#868e96", + "strokeColor": "#1e1e1e", "backgroundColor": "#e9ecef", "fillStyle": "solid", "strokeWidth": 4, @@ -3792,11 +3792,11 @@ "index": "b7b", "roundness": null, "seed": 174940230, - "version": 1347, - "versionNonce": 1044738265, + "version": 1348, + "versionNonce": 1935531705, "isDeleted": false, "boundElements": [], - "updated": 1739350261695, + "updated": 1741779765737, "link": null, "locked": false, "text": "per Traffic type and Environment data pipeline", @@ -3813,11 +3813,11 @@ "id": "4yw81UNgU1rCORZbk4stH", "type": "rectangle", "x": 523.9542141991748, - "y": 1112.8233135766554, + "y": 1113.026487037567, "width": 322.86495281700456, "height": 278.6516910012911, "angle": 0, - "strokeColor": "#868e96", + "strokeColor": "#1e1e1e", "backgroundColor": "#e9ecef", "fillStyle": "solid", "strokeWidth": 4, @@ -3829,11 +3829,11 @@ "index": "b7c", "roundness": null, "seed": 262498182, - "version": 779, - "versionNonce": 1202283831, + "version": 786, + "versionNonce": 651982585, "isDeleted": false, "boundElements": [], - "updated": 1739350261695, + "updated": 1741788547488, "link": null, "locked": false }, @@ -3845,7 +3845,7 @@ "width": 321.67248430894335, "height": 1.5101659077943168, "angle": 0, - "strokeColor": "#868e96", + "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 4, @@ -3857,11 +3857,11 @@ "index": "b7d", "roundness": null, "seed": 383420102, - "version": 3038, - "versionNonce": 1844702649, + "version": 3039, + "versionNonce": 827898137, "isDeleted": false, "boundElements": [], - "updated": 1739350261695, + "updated": 1741779830032, "link": null, "locked": false, "points": [ @@ -3888,7 +3888,7 @@ "width": 122.8752022449216, "height": 25.403009477994473, "angle": 0, - "strokeColor": "#868e96", + "strokeColor": "#1e1e1e", "backgroundColor": "#e9ecef", "fillStyle": "solid", "strokeWidth": 4, @@ -3900,11 +3900,11 @@ "index": "b7e", "roundness": null, "seed": 279559686, - "version": 982, - "versionNonce": 232105559, + "version": 983, + "versionNonce": 318653561, "isDeleted": false, "boundElements": [], - "updated": 1739350261695, + "updated": 1741779770987, "link": null, "locked": false, "text": "Data collected", @@ -3925,7 +3925,7 @@ "width": 296.25935563156884, "height": 19.69077613476543, "angle": 0, - "strokeColor": "#868e96", + "strokeColor": "#1e1e1e", "backgroundColor": "#e9ecef", "fillStyle": "solid", "strokeWidth": 4, @@ -3937,11 +3937,11 @@ "index": "b7f", "roundness": null, "seed": 1376783686, - "version": 736, - "versionNonce": 545246873, + "version": 737, + "versionNonce": 1663601175, "isDeleted": false, "boundElements": [], - "updated": 1739350261695, + "updated": 1741779833387, "link": null, "locked": false }, @@ -3953,7 +3953,7 @@ "width": 296.25935563156884, "height": 19.69077613476543, "angle": 0, - "strokeColor": "#868e96", + "strokeColor": "#1e1e1e", "backgroundColor": "#e9ecef", "fillStyle": "solid", "strokeWidth": 4, @@ -3965,11 +3965,11 @@ "index": "b7g", "roundness": null, "seed": 141194374, - "version": 867, - "versionNonce": 52216695, + "version": 870, + "versionNonce": 411547033, "isDeleted": false, "boundElements": [], - "updated": 1739350261695, + "updated": 1741788713711, "link": null, "locked": false }, @@ -3981,7 +3981,7 @@ "width": 297.08937362852095, "height": 19.69077613476543, "angle": 0, - "strokeColor": "#868e96", + "strokeColor": "#1e1e1e", "backgroundColor": "#e9ecef", "fillStyle": "solid", "strokeWidth": 4, @@ -3993,23 +3993,23 @@ "index": "b7h", "roundness": null, "seed": 762704838, - "version": 871, - "versionNonce": 1553765241, + "version": 872, + "versionNonce": 1878770455, "isDeleted": false, "boundElements": [], - "updated": 1739350261695, + "updated": 1741779856283, "link": null, "locked": false }, { "id": "SAorSlU0M51LJxoS93tFD", "type": "ellipse", - "x": 536.6609057550584, + "x": 537.5597377186416, "y": 1353.2063970197937, - "width": 296.25935563156884, + "width": 295.36052366798566, "height": 19.69077613476543, "angle": 0, - "strokeColor": "#868e96", + "strokeColor": "#1e1e1e", "backgroundColor": "#e9ecef", "fillStyle": "solid", "strokeWidth": 4, @@ -4021,186 +4021,14 @@ "index": "b7i", "roundness": null, "seed": 2068810502, - "version": 891, - "versionNonce": 935541911, + "version": 1007, + "versionNonce": 172377079, "isDeleted": false, "boundElements": [], - "updated": 1739350261695, + "updated": 1741788649434, "link": null, "locked": false }, - { - "id": "fkQ22j67eZEJGu_c2YnKN", - "type": "line", - "x": 537.124037483747, - "y": 1177.7151831058018, - "width": 0.5961650229149882, - "height": 66.24710513356285, - "angle": 0, - "strokeColor": "#868e96", - "backgroundColor": "#e9ecef", - "fillStyle": "solid", - "strokeWidth": 4, - "strokeStyle": "solid", - "roughness": 0, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b7k", - "roundness": null, - "seed": 1618229830, - "version": 616, - "versionNonce": 109952089, - "isDeleted": false, - "boundElements": [], - "updated": 1739350261695, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -0.5961650229149882, - 66.24710513356285 - ] - ], - "lastCommittedPoint": null, - "startBinding": null, - "endBinding": null, - "startArrowhead": null, - "endArrowhead": null - }, - { - "id": "VzS6YWF6PJe-guC5XLUCL", - "type": "line", - "x": 835.7175953460737, - "y": 1179.4199620910258, - "width": 0.6620204614927161, - "height": 63.32520330613693, - "angle": 0, - "strokeColor": "#868e96", - "backgroundColor": "#e9ecef", - "fillStyle": "solid", - "strokeWidth": 4, - "strokeStyle": "solid", - "roughness": 0, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b7l", - "roundness": null, - "seed": 1942028678, - "version": 870, - "versionNonce": 1065193911, - "isDeleted": false, - "boundElements": [], - "updated": 1739350261695, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -0.6620204614927161, - 63.32520330613693 - ] - ], - "lastCommittedPoint": null, - "startBinding": null, - "endBinding": null, - "startArrowhead": null, - "endArrowhead": null - }, - { - "id": "StgdujzXHoOH3VvBOdSuC", - "type": "line", - "x": 537.3054058660856, - "y": 1294.4918154985885, - "width": 0.8387903229384506, - "height": 69.10315152241105, - "angle": 0, - "strokeColor": "#868e96", - "backgroundColor": "#e9ecef", - "fillStyle": "solid", - "strokeWidth": 4, - "strokeStyle": "solid", - "roughness": 0, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b7m", - "roundness": null, - "seed": 1752359110, - "version": 962, - "versionNonce": 774293817, - "isDeleted": false, - "boundElements": [], - "updated": 1739350261695, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -0.8387903229384506, - 69.10315152241105 - ] - ], - "lastCommittedPoint": null, - "startBinding": null, - "endBinding": null, - "startArrowhead": null, - "endArrowhead": null - }, - { - "id": "DcQPI0c1N9v_Hir93fqGS", - "type": "line", - "x": 834.9286944490725, - "y": 1296.1353295308172, - "width": 1.0640852443889344, - "height": 66.0010137578247, - "angle": 0, - "strokeColor": "#868e96", - "backgroundColor": "#e9ecef", - "fillStyle": "solid", - "strokeWidth": 4, - "strokeStyle": "solid", - "roughness": 0, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b7n", - "roundness": null, - "seed": 36100102, - "version": 956, - "versionNonce": 1131430615, - "isDeleted": false, - "boundElements": [], - "updated": 1739350261695, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -1.0640852443889344, - 66.0010137578247 - ] - ], - "lastCommittedPoint": null, - "startBinding": null, - "endBinding": null, - "startArrowhead": null, - "endArrowhead": null - }, { "id": "j9W6vQQvYAnJmrhmFpoCU", "type": "text", @@ -4209,7 +4037,7 @@ "width": 87.35993134975433, "height": 21.6, "angle": 0, - "strokeColor": "#868e96", + "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 4, @@ -4221,11 +4049,11 @@ "index": "b7o", "roundness": null, "seed": 148498246, - "version": 1242, - "versionNonce": 483080729, + "version": 1243, + "versionNonce": 1826523929, "isDeleted": false, "boundElements": [], - "updated": 1739350261695, + "updated": 1741779779199, "link": null, "locked": false, "text": "Impressions", @@ -4242,11 +4070,11 @@ "id": "JvJBOb0L9NRFoO-toPso5", "type": "text", "x": 658.2786125143814, - "y": 1303.7413245261278, + "y": 1308.7413245261278, "width": 49.18396699428558, "height": 21.6, "angle": 0, - "strokeColor": "#868e96", + "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 4, @@ -4258,11 +4086,11 @@ "index": "b7p", "roundness": null, "seed": 1021906566, - "version": 1355, - "versionNonce": 1412419575, + "version": 1361, + "versionNonce": 491668919, "isDeleted": false, "boundElements": [], - "updated": 1739350261695, + "updated": 1741780783271, "link": null, "locked": false, "text": "Events", @@ -4278,10 +4106,10 @@ { "id": "hNQIgob1OtZuQG1CbyR_t", "type": "rectangle", - "x": 538.4788624673772, + "x": 538.4003872126967, "y": 1214.695042155753, - "width": 292.9593099545348, - "height": 29.297380114207957, + "width": 294.6594873234049, + "height": 29.872211354742166, "angle": 0, "strokeColor": "transparent", "backgroundColor": "#e9ecef", @@ -4295,20 +4123,20 @@ "index": "b7q", "roundness": null, "seed": 1259425222, - "version": 1255, - "versionNonce": 594448121, + "version": 1363, + "versionNonce": 308943479, "isDeleted": false, "boundElements": [], - "updated": 1739350261695, + "updated": 1741788691482, "link": null, "locked": false }, { "id": "ulB9ACyu4BBw1ymrSepsp", "type": "rectangle", - "x": 539.1104420029125, - "y": 1337.103689942127, - "width": 292.8383389012374, + "x": 538.2494353199345, + "y": 1338.103689942127, + "width": 293.6874562725278, "height": 25.104786401004503, "angle": 0, "strokeColor": "transparent", @@ -4323,23 +4151,23 @@ "index": "b7s", "roundness": null, "seed": 1081058566, - "version": 1707, - "versionNonce": 2076227863, + "version": 1973, + "versionNonce": 1505713913, "isDeleted": false, "boundElements": [], - "updated": 1739350261695, + "updated": 1741788697057, "link": null, "locked": false }, { "id": "SksWec6vEIt8zhdvg0tXy", "type": "text", - "x": 541.7817446345384, - "y": 1207.3028656012375, + "x": 545.190511455912, + "y": 1205.6446356422775, "width": 287.6522537644755, - "height": 42.03778021664115, + "height": 42.03778021664114, "angle": 0, - "strokeColor": "#868e96", + "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 4, @@ -4351,32 +4179,32 @@ "index": "b7t", "roundness": null, "seed": 2122388550, - "version": 2644, - "versionNonce": 680806361, + "version": 3087, + "versionNonce": 769626009, "isDeleted": false, "boundElements": [], - "updated": 1739350261695, + "updated": 1741788804075, "link": null, "locked": false, - "text": "(logs of rule evaluations, sent from SDK\nor Split Evaluator)", + "text": "(logs of rule evaluations, sent from SDK \n or Split Evaluator)", "fontSize": 15.569548228385607, "fontFamily": 6, "textAlign": "left", "verticalAlign": "top", "containerId": null, - "originalText": "(logs of rule evaluations, sent from SDK or Split Evaluator)", + "originalText": "(logs of rule evaluations, sent from SDK \n or Split Evaluator)", "autoResize": false, "lineHeight": 1.35 }, { "id": "N59csCQ3kB1qyacr5PudW", "type": "text", - "x": 542.1470678006331, - "y": 1323.0535929744105, - "width": 304.6860235849729, - "height": 43.2, + "x": 548.278905391419, + "y": 1338.1854305651964, + "width": 270.24744335064736, + "height": 21.6, "angle": 0, - "strokeColor": "#868e96", + "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 4, @@ -4388,20 +4216,20 @@ "index": "b7u", "roundness": null, "seed": 790797190, - "version": 2497, - "versionNonce": 261586487, + "version": 2565, + "versionNonce": 92748345, "isDeleted": false, "boundElements": [], - "updated": 1739350261695, + "updated": 1741788539514, "link": null, "locked": false, - "text": "(performance and behavioral data, sent\nfrom SDK, API, or integrations)", + "text": "(sent from SDK, API, or integrations)", "fontSize": 16, "fontFamily": 6, "textAlign": "left", "verticalAlign": "top", "containerId": null, - "originalText": "(performance and behavioral data, sent from SDK, API, or integrations)", + "originalText": "(sent from SDK, API, or integrations)", "autoResize": false, "lineHeight": 1.35 }, @@ -4413,7 +4241,7 @@ "width": 200.48128540942957, "height": 0.14557518001424796, "angle": 0, - "strokeColor": "#868e96", + "strokeColor": "#1e1e1e", "backgroundColor": "#e9ecef", "fillStyle": "solid", "strokeWidth": 4, @@ -4425,11 +4253,11 @@ "index": "b7w", "roundness": null, "seed": 949041862, - "version": 666, - "versionNonce": 1984947385, + "version": 667, + "versionNonce": 2107827095, "isDeleted": false, "boundElements": [], - "updated": 1739350261695, + "updated": 1741779907241, "link": null, "locked": false, "points": [ @@ -4451,12 +4279,12 @@ { "id": "snBNpuFbFziQAEReF4cDV", "type": "line", - "x": 865.5780745469106, - "y": 1291.5921479548028, + "x": 864.5780745469106, + "y": 1293.5921479548028, "width": 197.32022435769454, "height": 1.7434360844549701, "angle": 0, - "strokeColor": "#868e96", + "strokeColor": "#1e1e1e", "backgroundColor": "#e9ecef", "fillStyle": "solid", "strokeWidth": 4, @@ -4468,11 +4296,11 @@ "index": "b7z", "roundness": null, "seed": 829026822, - "version": 685, - "versionNonce": 1117298519, + "version": 689, + "versionNonce": 570213047, "isDeleted": false, "boundElements": [], - "updated": 1739350261695, + "updated": 1741788558523, "link": null, "locked": false, "points": [ @@ -4499,7 +4327,7 @@ "width": 48.762588460183906, "height": 51.585229858286084, "angle": 0, - "strokeColor": "#868e96", + "strokeColor": "#1e1e1e", "backgroundColor": "#e9ecef", "fillStyle": "solid", "strokeWidth": 4, @@ -4511,11 +4339,11 @@ "index": "b81", "roundness": null, "seed": 1420411206, - "version": 796, - "versionNonce": 1212474777, + "version": 797, + "versionNonce": 591007961, "isDeleted": false, "boundElements": [], - "updated": 1739350261695, + "updated": 1741779918337, "link": null, "locked": false, "points": [ @@ -4539,10 +4367,10 @@ "type": "line", "x": 1116.9716515627076, "y": 1238.3341353353571, - "width": 53.92446016292024, - "height": 51.270305358964606, + "width": 52.923380118823616, + "height": 53.10363722119382, "angle": 0, - "strokeColor": "#868e96", + "strokeColor": "#1e1e1e", "backgroundColor": "#e9ecef", "fillStyle": "solid", "strokeWidth": 4, @@ -4554,11 +4382,11 @@ "index": "b82", "roundness": null, "seed": 1974315142, - "version": 846, - "versionNonce": 1998101623, + "version": 881, + "versionNonce": 450735705, "isDeleted": false, "boundElements": [], - "updated": 1739350261695, + "updated": 1741788586715, "link": null, "locked": false, "points": [ @@ -4567,8 +4395,8 @@ 0 ], [ - -53.92446016292024, - 51.270305358964606 + -52.923380118823616, + 53.10363722119382 ] ], "lastCommittedPoint": null, @@ -4585,7 +4413,7 @@ "width": 49.117758951903056, "height": 53.48848042804116, "angle": 0, - "strokeColor": "#868e96", + "strokeColor": "#1e1e1e", "backgroundColor": "#e9ecef", "fillStyle": "solid", "strokeWidth": 4, @@ -4597,11 +4425,11 @@ "index": "b83", "roundness": null, "seed": 1949476806, - "version": 903, - "versionNonce": 430361209, + "version": 904, + "versionNonce": 349317591, "isDeleted": false, "boundElements": [], - "updated": 1739350261695, + "updated": 1741779911999, "link": null, "locked": false, "points": [ @@ -4628,7 +4456,7 @@ "width": 52.642757953673254, "height": 54.08464545095558, "angle": 0, - "strokeColor": "#868e96", + "strokeColor": "#1e1e1e", "backgroundColor": "#e9ecef", "fillStyle": "solid", "strokeWidth": 4, @@ -4640,11 +4468,11 @@ "index": "b84", "roundness": null, "seed": 810925830, - "version": 935, - "versionNonce": 1356592535, + "version": 936, + "versionNonce": 2015314713, "isDeleted": false, "boundElements": [], - "updated": 1739350261695, + "updated": 1741779911999, "link": null, "locked": false, "points": [ @@ -4671,7 +4499,7 @@ "width": 100.86063310722847, "height": 27.752608715469282, "angle": 0, - "strokeColor": "#868e96", + "strokeColor": "#1e1e1e", "backgroundColor": "#e9ecef", "fillStyle": "solid", "strokeWidth": 4, @@ -4683,11 +4511,11 @@ "index": "b85", "roundness": null, "seed": 2123624006, - "version": 1571, - "versionNonce": 859378521, + "version": 1572, + "versionNonce": 2061214809, "isDeleted": false, "boundElements": [], - "updated": 1739350261695, + "updated": 1741779803428, "link": null, "locked": false, "text": "Attribution", @@ -4708,7 +4536,7 @@ "width": 300.7895165905795, "height": 99.47637300964061, "angle": 0, - "strokeColor": "#868e96", + "strokeColor": "#1e1e1e", "backgroundColor": "#e9ecef", "fillStyle": "solid", "strokeWidth": 4, @@ -4720,55 +4548,13 @@ "index": "b86", "roundness": null, "seed": 466236806, - "version": 697, - "versionNonce": 118958775, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "JsSVEiuOvo1RATrsiXk_1" - } - ], - "updated": 1739350261695, - "link": null, - "locked": false - }, - { - "id": "JsSVEiuOvo1RATrsiXk_1", - "type": "text", - "x": 1161.449507976046, - "y": 1155.5430616678182, - "width": 245.53971314430237, - "height": 27, - "angle": 0, - "strokeColor": "#868e96", - "backgroundColor": "#e9ecef", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 0, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b87", - "roundness": null, - "seed": 2016711863, - "version": 578, - "versionNonce": 2035236921, + "version": 699, + "versionNonce": 1215758199, "isDeleted": false, "boundElements": [], - "updated": 1739350261695, + "updated": 1741779996090, "link": null, - "locked": false, - "text": "Metrics impact calculations", - "fontSize": 20, - "fontFamily": 6, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "Wt2ShljiVAJhWBgunP-aZ", - "originalText": "Metrics impact calculations", - "autoResize": true, - "lineHeight": 1.35 + "locked": false }, { "id": "mEscpLpWz9Z3TtAIYVEHj", @@ -4778,7 +4564,7 @@ "width": 300.7895165905795, "height": 99.47637300964061, "angle": 0, - "strokeColor": "#868e96", + "strokeColor": "#1e1e1e", "backgroundColor": "#e9ecef", "fillStyle": "solid", "strokeWidth": 4, @@ -4790,8 +4576,8 @@ "index": "b88", "roundness": null, "seed": 557159622, - "version": 847, - "versionNonce": 1986968535, + "version": 848, + "versionNonce": 111883383, "isDeleted": false, "boundElements": [ { @@ -4799,7 +4585,7 @@ "id": "CEI39PqnhrpfiIZnIsmhC" } ], - "updated": 1739350261695, + "updated": 1741779814477, "link": null, "locked": false }, @@ -4811,7 +4597,7 @@ "width": 161.7198028564453, "height": 27, "angle": 0, - "strokeColor": "#868e96", + "strokeColor": "#1e1e1e", "backgroundColor": "#e9ecef", "fillStyle": "solid", "strokeWidth": 2, @@ -4823,11 +4609,11 @@ "index": "b89", "roundness": null, "seed": 551649303, - "version": 748, - "versionNonce": 2043466009, + "version": 749, + "versionNonce": 1948889721, "isDeleted": false, "boundElements": [], - "updated": 1739350261695, + "updated": 1741779814477, "link": null, "locked": false, "text": "Alert notifications", @@ -4844,11 +4630,11 @@ "id": "M1QgBcYqR_C2qD_HfSYvP", "type": "text", "x": 922.55172132051, - "y": 1221.4203692138612, + "y": 1221.5437011536287, "width": 168.6990960361816, "height": 60.772280534996646, "angle": 0, - "strokeColor": "#868e96", + "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 4, @@ -4860,11 +4646,11 @@ "index": "b8A", "roundness": null, "seed": 2020805638, - "version": 3547, - "versionNonce": 1928800503, + "version": 3549, + "versionNonce": 614823673, "isDeleted": false, "boundElements": [], - "updated": 1739350261695, + "updated": 1741779806274, "link": null, "locked": false, "text": "(associates events to\nFeature flag treatments\n[variations])", @@ -5004,7 +4790,7 @@ "x": 1372.8822760400387, "y": 253.6395669000973, "width": 167.6912774155525, - "height": 160.49295373885022, + "height": 220.67781139091886, "angle": 0, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", @@ -5018,20 +4804,20 @@ "index": "b8F", "roundness": null, "seed": 2058143814, - "version": 2839, - "versionNonce": 1818655834, + "version": 2854, + "versionNonce": 1302547287, "isDeleted": false, "boundElements": [], - "updated": 1739279605637, + "updated": 1741781633288, "link": null, "locked": false, - "text": "(Name, Owners, Tags,\nHypothesis, Assignment\nsource [Feature flag,\nEnvironment],\nExperiment scope [Start,\nEnd, Targeting rule,\nBaseline treatment,\nComparison treatments])", + "text": "(Name, Owners, Tags,\nHypothesis, \nAssignment source\n[Feature flag,\nEnvironment], \nScope [Start, End,\nBaseline treatment,\nComparison treatments,\nTargeting rule], Key\nmetrics, Supporting\nmetrics)", "fontSize": 14.860458679523154, "fontFamily": 6, "textAlign": "left", "verticalAlign": "top", "containerId": null, - "originalText": "(Name, Owners, Tags, Hypothesis, Assignment source [Feature flag, Environment], Experiment scope [Start, End, Targeting rule, Baseline treatment, Comparison treatments])", + "originalText": "(Name, Owners, Tags, Hypothesis, \nAssignment source [Feature flag, Environment], \nScope [Start, End, Baseline treatment, Comparison treatments, Targeting rule], Key metrics, Supporting metrics)", "autoResize": false, "lineHeight": 1.35 }, @@ -5129,11 +4915,11 @@ "index": "b8I", "roundness": null, "seed": 45412473, - "version": 1760, - "versionNonce": 548348375, + "version": 1800, + "versionNonce": 1319476793, "isDeleted": false, "boundElements": [], - "updated": 1739350222503, + "updated": 1741781808544, "link": null, "locked": false, "text": "(always scoped to\none environment)", @@ -5145,6 +4931,252 @@ "originalText": "(always scoped to one environment)", "autoResize": false, "lineHeight": 1.35 + }, + { + "id": "ZWHkhATCg2I-xaYQzW7yE", + "type": "text", + "x": 1157.1759921424357, + "y": 1143.181771382353, + "width": 251.42904663085938, + "height": 27.752608715469275, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b8K", + "roundness": null, + "seed": 2126503993, + "version": 1981, + "versionNonce": 1663964505, + "isDeleted": false, + "boundElements": [], + "updated": 1741780003300, + "link": null, + "locked": false, + "text": "Metrics impact calculations", + "fontSize": 20.557487937384646, + "fontFamily": 6, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Metrics impact calculations", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "_ccDKZIYB4iWUfbJQ7kTj", + "type": "text", + "x": 1157.6306366543058, + "y": 1177.8586665606854, + "width": 239.13439211993503, + "height": 20.257426844998882, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b8L", + "roundness": null, + "seed": 1210675481, + "version": 4008, + "versionNonce": 1184406969, + "isDeleted": false, + "boundElements": [], + "updated": 1741780008406, + "link": null, + "locked": false, + "text": "(Key metrics, Supporting metrics)", + "fontSize": 15.005501366665838, + "fontFamily": 6, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(Key metrics, Supporting metrics)", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "id": "fkQ22j67eZEJGu_c2YnKN", + "type": "line", + "x": 537.124037483747, + "y": 1177.7151831058018, + "width": 0.5961650229149882, + "height": 66.24710513356285, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b8M", + "roundness": null, + "seed": 1618229830, + "version": 618, + "versionNonce": 354952599, + "isDeleted": false, + "boundElements": [], + "updated": 1741788725159, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.5961650229149882, + 66.24710513356285 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "VzS6YWF6PJe-guC5XLUCL", + "type": "line", + "x": 833.7175953460737, + "y": 1179.4199620910258, + "width": 0.8071996826514578, + "height": 65.08402995166239, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b8N", + "roundness": null, + "seed": 1942028678, + "version": 909, + "versionNonce": 549390711, + "isDeleted": false, + "boundElements": [], + "updated": 1741788738729, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.8071996826514578, + 65.08402995166239 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "StgdujzXHoOH3VvBOdSuC", + "type": "line", + "x": 537.3652432477794, + "y": 1293.268582466257, + "width": 0.8986277046322471, + "height": 70.32638455474262, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b8O", + "roundness": null, + "seed": 1752359110, + "version": 984, + "versionNonce": 226793303, + "isDeleted": false, + "boundElements": [], + "updated": 1741788751481, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.8986277046322471, + 70.32638455474262 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "DcQPI0c1N9v_Hir93fqGS", + "type": "line", + "x": 834.9286944490725, + "y": 1292.957081716259, + "width": 1.8194095706883218, + "height": 70.20630646801328, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b8P", + "roundness": null, + "seed": 36100102, + "version": 1047, + "versionNonce": 526574905, + "isDeleted": false, + "boundElements": [], + "updated": 1741788766880, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -1.8194095706883218, + 70.20630646801328 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null } ], "appState": {