-
Notifications
You must be signed in to change notification settings - Fork 48
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
Conversation
Thanks for contributing, the idea make sense. |
There was a problem hiding this 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.
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? |
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; |
There was a problem hiding this comment.
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;
}
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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)
Feature/prevent fetch
Oh, there are a few lint issues. Do you mind running "yarn format" on it to automatically fix these @Florent-Wanteeed @jeremiewtd ? |
There was a problem hiding this 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.
const timestamp = Date.now(); | ||
|
||
return !!( | ||
this.lastRefreshTimestamp && | ||
this.lastRefreshTimestamp <= timestamp && | ||
timestamp - this.lastRefreshTimestamp <= | ||
this.experimental.togglesStorageTTL! | ||
); |
There was a problem hiding this comment.
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
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 | |
); |
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; | ||
}; |
There was a problem hiding this comment.
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 || {}),
});
};
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 { |
There was a problem hiding this comment.
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)
Merged in #224 |
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