Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: prevent fetching toggles when they are up to date #202

Closed
wants to merge 43 commits into from

Conversation

jeremiewtd
Copy link
Contributor

@jeremiewtd jeremiewtd commented May 6, 2024

About the changes

This pull request is a proposal for fixing this issue to add compatibility with service worker and browser extensions to avoid multiple requests to the proxy server each time the service worker is started.

It introduces a new option togglesStorageTTL that prevents fetching the flags if they are up to date (not older than the number of seconds defined in this option).

A second pull request will come to fix the "refresh" problem described in the issue.

Closes #201

Important files

Main changes concerned index.ts file.

Discussion points

@jeremiewtd jeremiewtd changed the title Prevent fetching flags when they are up to date Prevent fetching toggles when they are up to date May 6, 2024
@jeremiewtd jeremiewtd changed the title Prevent fetching toggles when they are up to date Feat: prevent fetching toggles when they are up to date May 6, 2024
@jeremiewtd jeremiewtd marked this pull request as ready for review May 6, 2024 11:13
@ivarconr ivarconr self-requested a review May 6, 2024 12:33
@ivarconr
Copy link
Member

ivarconr commented May 6, 2024

Thanks for contributing, the idea make sense.
We will try to review it this week.

README.md Outdated Show resolved Hide resolved
Copy link
Contributor Author

@jeremiewtd jeremiewtd left a comment

Choose a reason for hiding this comment

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

We made some changes based on your review suggestions, we are waiting for your answers to the remaining questions before doing anything else.

@jeremiewtd
Copy link
Contributor Author

Come to think of it, should the context be part of the timestamp "key"? Otherwise the saved toggles could be wrong if they were saved in a different context than the current starting one in a new instance?

@ivarconr
Copy link
Member

Come to think of it, should the context be part of the timestamp "key"? Otherwise the saved toggles could be wrong if they were saved in a different context than the current starting one in a new instance?

This makes me think we could store the etag as well. This would yield a validation call to the API but return a 304 if nothing changed. In terms of Unleash/Edge that is a lot cheaper than a full 200 response. Would that be good enough, or do you still see the need to avoid the initial HTTP call?

If we keep the TTL approach we should definitely store a hash of the context object, to make sure the context has not changed in between.

@ivarconr ivarconr self-assigned this May 16, 2024
@jeremiewtd
Copy link
Contributor Author

Come to think of it, should the context be part of the timestamp "key"? Otherwise the saved toggles could be wrong if they were saved in a different context than the current starting one in a new instance?

This makes me think we could store the etag as well. This would yield a validation call to the API but return a 304 if nothing changed. In terms of Unleash/Edge that is a lot cheaper than a full 200 response. Would that be good enough, or do you still see the need to avoid the initial HTTP call?

If we keep the TTL approach we should definitely store a hash of the context object, to make sure the context has not changed in between.

The Etag seems like a good idea in absolute terms but it doesn't solve our main problem which is the number of calls to the server. Do you think we should continue with this PR or do you prefer to only use the etag?

@ivarconr
Copy link
Member

The Etag seems like a good idea in absolute terms but it doesn't solve our main problem which is the number of calls to the server. Do you think we should continue with this PR or do you prefer to only use the etag?

When I think about it feels like two different needs. I would like to keep the TTL separate. In order to test this feature more widely before we commit I would also like to have the option as an experimental option and not part of the official API just yet.

@jeremiewtd
Copy link
Contributor Author

The Etag seems like a good idea in absolute terms but it doesn't solve our main problem which is the number of calls to the server. Do you think we should continue with this PR or do you prefer to only use the etag?

When I think about it feels like two different needs. I would like to keep the TTL separate. In order to test this feature more widely before we commit I would also like to have the option as an experimental option and not part of the official API just yet.

OK, how can we do this experimental option?

@@ -53,6 +53,7 @@ interface IConfig extends IStaticContext {
customHeaders?: Record<string, string>;
impressionDataAll?: boolean;
usePOSTrequests?: boolean;
togglesStorageTTL?: number;
Copy link
Member

Choose a reason for hiding this comment

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

Instead of adding it directly to the config definition, If we could have an optional "experimental" container we could make it clear that this option might go away or be changed in the future.

experimental?: {
  togglesStorageTTL?: number;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok we are working on it and adding the hashed context. Do you have any proposal for how you would like this to be done?

Copy link
Contributor

Choose a reason for hiding this comment

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

Hello

Concerning the hashed context, we started with the use of this type of library : “object-hash”.
Is this OK for you ?

unleash-proxy-client” lib size => 5.5 KB
object-hash” lib size => 10 KB

Which is a significant increase in percentage terms.

Last lib update: 02/2022 (v3.0.0),

Otherwise we can simply use a JSON.stringify (more verbose but which does not depend on anything)

Copy link
Member

Choose a reason for hiding this comment

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

We do not want to take on a library of this size for this.

I think the JSON.stringify is a good start, and as this is an experimental feature we can use that to learn the performance impact here. In the future we can consider to take advantage of the built in hasing capabilities, supported by most modern browsers (https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest)

@ivarconr ivarconr changed the title Feat: prevent fetching toggles when they are up to date feat: prevent fetching toggles when they are up to date Jun 24, 2024
@ivarconr ivarconr requested a review from kwasniew July 25, 2024 08:06
@ivarconr
Copy link
Member

Oh, there are a few lint issues. Do you mind running "yarn format" on it to automatically fix these @Florent-Wanteeed @jeremiewtd ?

Copy link
Member

@Tymek Tymek left a comment

Choose a reason for hiding this comment

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

Neat! I'll test it in practical setup. What I also would like to see, maybe later on - not in this PR, is the use of proper hashing. Better not to persist PII when there is no need to do it. JSON.stringify can be a fallback.

Comment on lines +497 to +504
const timestamp = Date.now();

return !!(
this.lastRefreshTimestamp &&
this.lastRefreshTimestamp <= timestamp &&
timestamp - this.lastRefreshTimestamp <=
this.experimental.togglesStorageTTL!
);
Copy link
Member

Choose a reason for hiding this comment

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

This logic was a bit hard to read

Suggested change
const timestamp = Date.now();
return !!(
this.lastRefreshTimestamp &&
this.lastRefreshTimestamp <= timestamp &&
timestamp - this.lastRefreshTimestamp <=
this.experimental.togglesStorageTTL!
);
const now = Date.now();
const ttl = this.experimental.togglesStorageTTL || 0;
return (
this.lastRefreshTimestamp > 0 &&
this.lastRefreshTimestamp <= now &&
now - this.lastRefreshTimestamp <= ttl
);

Comment on lines +30 to +46
const sortObjectProperties = (
obj: Record<string, unknown>
): Record<string, unknown> => {
const sortedKeys = Object.keys(obj).sort();
const sortedObj: Record<string, unknown> = {};
sortedKeys.forEach((key) => {
if (obj[key] !== null && typeof obj[key] === 'object') {
sortedObj[key] = sortObjectProperties(
obj[key] as Record<string, unknown>
);
} else {
sortedObj[key] = obj[key];
}
});

return sortedObj;
};
Copy link
Member

Choose a reason for hiding this comment

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

Context has a simple structure. I don't think we need recursion here.
https://docs.getunleash.io/reference/unleash-context#structure

Output doesn't need to directly correspond to context structure.

const sortEntries = (record: Record<string, string>) => {
    return Object.entries(record).sort(([a], [b]) => a.localeCompare(b));
};

const contextString = (context: IContext): string => {
    const { properties, ...fields } = context;

    return JSON.stringify({
        fields: sortEntries(fields),
        properties: sortEntries(context.properties || {}),
    });
};

src/util.test.ts Show resolved Hide resolved
Comment on lines +579 to +597
if (!response.ok) {
if (response.status === 304) {
this.storeLastRefreshTimestamp();
} else {
console.error(
'Unleash: Fetching feature toggles did not have an ok response'
);
this.sdkState = 'error';
this.emit(EVENTS.ERROR, {
type: 'HttpError',
code: response.status,
});

this.lastError = {
type: 'HttpError',
code: response.status,
};
}
} else {
Copy link
Member

Choose a reason for hiding this comment

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

Success-first statement is easier to read here, because there is no early return.

if (response.ok) {
} else if (response.status === 304) {
} else {}

(Using the fact that response.ok is a shorthand)

@Tymek Tymek requested a review from Florent-Wanteeed August 5, 2024 08:41
@Tymek Tymek mentioned this pull request Aug 5, 2024
@Tymek
Copy link
Member

Tymek commented Aug 6, 2024

Merged in #224

@Tymek Tymek closed this Aug 6, 2024
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.

Service worker compatibility
4 participants