Skip to content

feat: Use new evaluations endpoint#48

Merged
liamhughes merged 6 commits intomainfrom
liamhughes/devex-137-use-new-evaluations-endpoint
Apr 8, 2026
Merged

feat: Use new evaluations endpoint#48
liamhughes merged 6 commits intomainfrom
liamhughes/devex-137-use-new-evaluations-endpoint

Conversation

@liamhughes
Copy link
Copy Markdown
Contributor

@liamhughes liamhughes commented Apr 7, 2026

Background

The client percentage rollout feature requires ClientRolloutPercentage and EvaluationKey to be included in evaluation responses.

We were unable to add these fields to the existing GET /featuretoggles/v3 evaluations endpoint because the Java OpenFeature provider was previously using Jackson with strict deserialisation. See OctopusDeploy/openfeature-provider-java#5.

This PR updates this provider to use the new endpoint, which has the following changes compared to the old endpoint:

  • The name field was removed
  • evaluationKey and clientRolloutPercentage were added, though are only returned if isEnabled evaluated to true in the feature toggles service
  • segments are also only returned if isEnabled evaluated to true

Resolves DEVEX-137.

Changes

  • Update URI called by provider and in test WireMock server
  • Updates to V1FeatureToggleEvaluation interface
    • Removed name property
    • Added optional evaluationKey and clientRolloutPercentage properties
    • Updated segments to be optional
  • Updates to OctopusFeatureContext class
    • Type changes from V1FeatureToggleEvaluation
    • Added missingRequiredPropertiesForClientSideEvaluation check
  • Updated the localStorage backed caching so that old caches will not be used

Notes for review

Testing

  • Smoke test against a locally running instance of OctoToggle was successful.
  • All existing tests are passing, albeit with some change in test data.
  • The majority of specification tests at this commit pass, except those relying on the client rollout functionality. As such, I have disabled the specification tests because there isn't a commit in OctopusDeploy/openfeature-provider-specification that uses the new endpoint response shape that doesn't also expect the client percentage rollout feature to be implemented. This will be implemented and fully tested in the next PR, prior to the next release of this library
Screenshot 2026-04-07 at 14 38 36

@liamhughes liamhughes requested a review from Copilot April 7, 2026 05:37
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Updates the TypeScript OpenFeature provider to call the new Octopus “evaluations” endpoint and aligns the client-side evaluation model with the new response shape (removing name, adding rollout-related fields, and making segments optional).

Changes:

  • Switch provider/test server calls from /api/featuretoggles/v3/ to /api/toggles/evaluations/v3/.
  • Update V1FeatureToggleEvaluation to match the new endpoint (remove name, add optional evaluationKey/clientRolloutPercentage, make segments optional).
  • Add client-side validation for required evaluation fields and adjust segment evaluation logic; disable specification fixture tests for now.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/specificationTests/server.ts Updates mocked server route to the new evaluations endpoint.
src/specificationTests/fixtureEvaluationTests.test.ts Skips specification fixture tests pending rollout hashing support.
src/octopusFeatureProvider.test.ts Updates test evaluation objects to include new fields.
src/octopusFeatureContext.ts Updates evaluation interface/logic and adds required-field validation for enabled toggles.
src/octopusFeatureContext.test.ts Updates/extends tests for new evaluation shape and missing-field behavior.
src/octopusFeatureClient.ts Switches client request URL to the new evaluations endpoint.
src/octopusFeatureClient.test.ts Updates URL assertion to match the new endpoint.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +43 to +49
if (missingRequiredPropertiesForClientSideEvaluation(evaluation)) {
return {
value: defaultValue,
errorCode: ErrorCode.PARSE_ERROR,
errorMessage: `Feature toggle ${slug} is missing necessary information for client-side evaluation.`,
};
}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

The new missingRequiredFieldsForClientEvaluation PARSE_ERROR path will break fallback-to-cache behavior for users upgrading from the previous response shape: cached evaluations (schemaVersion "v1") won’t have evaluationKey/clientRolloutPercentage, so enabled flags will now resolve to defaultValue+PARSE_ERROR whenever the network fetch fails. Consider bumping the cache schema/localStorage key to invalidate older cached manifests, or relaxing this check until client-side rollout logic actually depends on these fields (e.g., only require them when performing rollout hashing).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Huh. The use of local storage could indeed be the source of bugs in the future... I'll find a fix and make a note to discuss this further.

Comment on lines +64 to +66
// Skipped: rollout percentage evaluation (hashing) not yet implemented.
// Re-enable when the follow-up PR adds that logic.
test.skip.each(testCases)("$testCase.description", async ({ testResponse, testCase }) => {
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

test.skip.each(...) still executes the module-level loadTestCases() (and fs.readdirSync) when Jest loads this file, so skipping won’t prevent failures if the specification/Fixtures submodule/directory isn’t present. If the intent is to disable these spec tests temporarily, consider guarding fixture loading with an existence check and conditionally skipping the suite, or making fixture loading lazy so it doesn’t run when skipped.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is fine. It's only until the next PR.

@OctopusDeploy OctopusDeploy deleted a comment from Copilot AI Apr 7, 2026
possibleV1CacheEntry.contents !== undefined &&
possibleV1CacheEntry.contents.evaluations !== undefined &&
possibleV1CacheEntry.contents.contentHash !== undefined
possibleV2CacheEntry.schemaVersion === "v2" &&
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This will ensure that previously cached entries won't be used with the new logic.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

src/octopusFeatureClient.ts:91

  • axios rejects non-2xx responses by default, so a 404 from this endpoint will throw and never reach the if (response.status == 404) branch. That prevents getEvaluationContext() from falling back to cached/empty toggles as intended. Consider adding validateStatus to allow 404 responses through, or wrapping the request in a try/catch and returning undefined when error.response?.status === 404 (and optionally for other expected failure modes).
        const response = await this.axiosInstance.request<V2FeatureToggleEvaluation[]>(config);

        if (response.status == 404) {
            this.logger.warn(`Failed to retrieve feature toggles for client identifier ${this.clientIdentifier} from ${this.serverUri}`);
            return undefined;

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 84 to 90
config.headers!["X-Release-Version"] = this.releaseVersionOverride;
}

const response = await this.axiosInstance.request<V1FeatureToggleEvaluation[]>(config);
const response = await this.axiosInstance.request<V2FeatureToggleEvaluation[]>(config);

if (response.status == 404) {
this.logger.warn(`Failed to retrieve feature toggles for client identifier ${this.clientIdentifier} from ${this.serverUri}`);
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

Within getFeatureManifest(), header parsing currently relies on response.headers.get("ContentHash") (with a // @ts-ignore). That isn’t safe across Axios adapters/versions because response.headers can be a plain object (no .get) and header names are often normalized to lowercase. Consider reading the header in a way that supports both AxiosHeaders and plain objects (case-insensitive), and remove the ts-ignore once types align.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've made a note of this for discussion/ticketing.

if (!evaluation.isEnabled) {
return false;
}
return evaluation.evaluationKey == null || evaluation.segments == null || evaluation.clientRolloutPercentage == null;
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

missingRequiredPropertiesForClientSideEvaluation requires segments/evaluationKey/clientRolloutPercentage for enabled toggles, but V2FeatureToggleEvaluation marks all three as optional and the current segment evaluation logic can already treat missing segments as “no segments”. This can cause enabled flags to return PARSE_ERROR + defaultValue for responses where the service omits these fields (e.g., enabled toggle with no segments). Consider either (a) modeling enabled vs disabled evaluations as a discriminated union (enabled => fields required), or (b) relaxing the check to only require fields when they’re actually needed (e.g. when rollout hashing is implemented) and treating segments undefined as an empty list.

Suggested change
return evaluation.evaluationKey == null || evaluation.segments == null || evaluation.clientRolloutPercentage == null;
// The current client-side evaluation only uses `isEnabled` and, optionally,
// `segments`. Missing `segments` is already treated as "no segments" by
// `evaluateSegments`, and rollout metadata is not yet used here.
return false;

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I already considered a discriminated union, but that still wouldn't protect against the API returning unexpected values (though we could think about implementing a schema check like zod).

I think Copilot is probably picking up the half-way nature of this PR here. We can tidy in the next PR.

export interface V2FeatureToggles {
evaluations: V2FeatureToggleEvaluation[];
contentHash: string;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe a silly question but what's the reason for renaming everything to V2?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

When I realised I had to update the cache entry, I updated these as well. I have gone with the assumption that the version number related to the shape of the objects rather than the API.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Makes sense ☺️ What would happen if we didn't update the cache entry?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Existing users of the consuming application may use evaluations without the new fields leading to incorrect client-side evaluations.

To be honest, I'm actually not sure how much value the cache is providing and I have a note to discuss it when Dylan gets back.

Copy link
Copy Markdown
Contributor

@caitlynstocker caitlynstocker left a comment

Choose a reason for hiding this comment

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

Looks in line with the .NET and Java versions 😊

@liamhughes liamhughes merged commit 48d8bf5 into main Apr 8, 2026
11 checks passed
@liamhughes liamhughes deleted the liamhughes/devex-137-use-new-evaluations-endpoint branch April 8, 2026 07:54
This was referenced Apr 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants