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

ajy-UID2-1667-Local-storage-for-v3-js-sdk #21

Merged
merged 21 commits into from
Oct 9, 2023

Conversation

alex-yau-ttd
Copy link
Contributor

@alex-yau-ttd alex-yau-ttd commented Sep 11, 2023

  • Add useCookie option for init.
  • Migrate legacy cookie to new cookie
  • Use local storage by default (when useCookie is not provided)
  • Use the new uid2StorageManager to manage cookie vs local storage
  • Prettier changes
  • Typo fixes

Tests

  • Create some new tests for the useCookie option in options.test.ts
  • Run lots of existing tests over three scenarios using testCookieAndLocalStorage.
  • Add some new mocks, and adjusting other mocks and their associated assertions

- Migrate legacy cookie to new cookie
- Add useCookie option
- use localStorage by default
@@ -71,12 +71,20 @@ export class UID2CookieManager {
}
}

private migrateLegacyCookie(identity: LegacyUid2SDKCookie, now: number): Uid2Identity {
const newCookie = enrichIdentity(identity, now);
this.setCookie(newCookie);
Copy link
Contributor

Choose a reason for hiding this comment

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

If the publisher is close to the cookie size limit, there's a chance this will fail - I think it'd be better to stick with the existing approach of enriching it and returning it, rather than storing the enriched cookie.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The enriched cookie is just the new cookie right? How do we plan to migrate from legacy cookie to the new (enriched) cookie if not by storing the enriched cookie? Can we just have them use local storage if they are close to the cookie size limit?

Copy link
Contributor

Choose a reason for hiding this comment

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

There's a good chance we'd be going from the legacy cookie straight into local storage, so if we just return it enriched here and store it in whatever is trying to upgrade it, then the common case won't ever increase the cookie size.

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually - my comment's not quite right.

A legacy cookie will eventually get replaced by a new one - the next time the identity is updated (probably when it gets refreshed). So if we just enrich it when we read it (and don't write it back), it'll eventually be stored in the new format - hopefully in local storage instead. But that then breaks the load, because the migrated value will look newer than anything in local storage. Hmmm.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thinking through a few options, I think maybe your initial approach is the right move here - just upgrade it and store it back immediately.

export class UID2LocalStorageManager {
public setValue(identity: Uid2Identity) {
const value = JSON.stringify(identity);
localStorage.setItem("identity", value);
Copy link
Contributor

Choose a reason for hiding this comment

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

identity probably has a high chance of conflicting with other things - I think it'd be a good idea to choose a localstorage ID that's prefixed with UID2.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

true. would UID2 itself also run the risk of conflicts? Or is it just too vague? Perhaps UID2-identity?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Alternatively, cookies use __uid_2. Would it make sense to use the same name or would that be more confusing?

Copy link
Contributor

Choose a reason for hiding this comment

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

We sometimes find publishers store things for UID2 as well - I think making it clear that it's stored by the SDK would be helpful in a lot of cases. Maybe something like UID2-sdk-identity.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done!

src/uid2Sdk.ts Outdated
this._cookieManager.setCookie(validity.identity);
}
else {
this._cookieManager.removeCookie();
Copy link
Contributor

Choose a reason for hiding this comment

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

I like the idea of removing the cookie here, but we'll need to be very sure there aren't any cases where the cookie might be removed if we haven't loaded it properly yet - we don't want to remove a valid cookie if we aren't definitely going to store it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

to be extra defensive, I could change getValue to a public method and do:

this._localStorageManager.setValue(validity.identity);
if (this._localStorageManager.getValue()) this._cookieManager.removeCookie();

Would that address your concern?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I think that's reasonable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done!

src/uid2Sdk.ts Outdated
identity = this._opts.identity;
} else {
const localStorageIdentity = this._localStorageManager.loadIdentityFromLocalStorage();
identity = localStorageIdentity !== null ? localStorageIdentity : this._cookieManager.loadIdentityFromCookie();
Copy link
Contributor

Choose a reason for hiding this comment

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

You could probably use ?? to simplify this a bit.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've changed how this works

src/uid2Sdk.ts Outdated
} else {
const localStorageIdentity = this._localStorageManager.loadIdentityFromLocalStorage();
const cookieIdentity = this._cookieManager.loadIdentityFromCookie();
const shouldUseCookie = cookieIdentity && (!localStorageIdentity || cookieIdentity.identity_expires > localStorageIdentity.identity_expires);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hopefully this handles the case we talked about @lionell-pack-ttd, where the cookie is newer than the value in localStorage, so we should use the cookie instead.

@@ -6,7 +6,7 @@ import { InitCallbackOptions } from "./Uid2InitCallbacks";
export type Uid2Options = BaseUid2Options &
InitCallbackOptions &
UID2CookieOptions &
Uid2ApiClientOptions;
Uid2ApiClientOptions & { useCookie?: boolean; };
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you could add useCookie to the base options type - I think that's just there for options that are just for uid2sdk.ts and not provided by one of the other classes/files.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done!

export class UID2LocalStorageManager {
public setValue(identity: Uid2Identity) {
const value = JSON.stringify(identity);
localStorage.setItem("UID2-identity", value);
Copy link
Contributor

Choose a reason for hiding this comment

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

Avoid magic strings. Make sure there's a single source of truth for UID2-identity - in some cases using a type to enforce the string being correct everywhere is an option, but I don't think we can do that here so a constant is probably the move.

Copy link
Contributor

Choose a reason for hiding this comment

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

As I mentioned in another comment, we often end up with a few different UID2 values from different sources (i.e. the publisher storing their own things), so I think something like UID2-sdk-identity would make it easier to trouble-shoot integrations.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

defined and exporting the const from this file now - I'm using it in the mocks as well (although I don't have to)

- Use const for localStorageKeyName
- Move useCookie to base options
- Check if value is in localStorage before removing cookie
- There are still 8 tests failing in 4 suites
Without this, only the first pass of the test with succeed, and the following ones would file since the window.cypto would not change
Wraps uid2CookieManager and uid2LocalStorageManager, so the sdk only needs the uid2StorageManager, offloading logic off the sdk class.
Also adjust the mock to return undefined instead of null for localStorage for parity with cookie mock
@alex-yau-ttd alex-yau-ttd marked this pull request as ready for review September 27, 2023 07:05
@@ -163,6 +177,20 @@ export function getUid2Cookie() {
}
}

export function removeUid2LocalStorage() {
localStorage.removeItem(localStorageKeyName);
Copy link
Contributor

Choose a reason for hiding this comment

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

this looks exact the same as removeValue in UID2LocalStorageManager? Why we couldn't use that instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed!


export function setUid2LocalStorage(identity: any) {
const value = JSON.stringify(identity);
localStorage.setItem(localStorageKeyName, value);
Copy link
Contributor

Choose a reason for hiding this comment

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

same as above

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed!

src/mocks.ts Outdated

export function getUid2LocalStorage() {
const value = localStorage.getItem(localStorageKeyName);
return value !== null ? JSON.parse(value) : undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we use loadIdentityFromLocalStorage?

Copy link
Contributor Author

@alex-yau-ttd alex-yau-ttd Sep 28, 2023

Choose a reason for hiding this comment

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

I see what you mean, my current implementation seems like a bit of copy-paste work and would still mean tests would pass if the actual functionality broke (BAD).

Are you suggesting that this mocks file should have a

const uid2LocalStorageManager = new UID2LocalStorageManager();

and then be

export function getUid2LocalStorage() {
  return uid2LocalStorageManager.loadIdentityFromLocalStorage();
}

Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we need them in the mocks.ts?
Could we do const uid2LocalStorageManager = new UID2LocalStorageManager(); in test file and just call uid2LocalStorageManager functions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah I think that makes sense. In fact I think I can actually use

const uid2StorageManager = new UID2StorageManager({});

then just do

uid2StorageManager.removeValues();

rather than having

  removeUid2Cookie();
  removeUid2LocalStorage();

Copy link
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 some changes, though I still have getUid2LocalStorage() as a mock so that it will return undefined instead of null. Alternatively, I could change loadIdentityFromLocalStorage() to return Uid2Identity | undefined instead of Uid2Identity | null and then remove the need for getUid2LocalStorage()

I initially based it off loadIdentityFromCookie(): Uid2Identity | null but I think maybe we can use undefined instead of null. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

tbh I am getting a bit confused myself. - perhaps @lionell-pack-ttd could chime in next week. I see in getUid2Cookie() we can return undefined (so this is what the tests expect), whereas in loadIdentityFromCookie() we can return null. Would it make sense to just use null or undefined everywhere, across SDK, tests, cookies & local storage?

Copy link
Contributor

Choose a reason for hiding this comment

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

What are we achieving by switching from the original approach to loadIdentityFromLocalStorage? Test infrastructure shouldn't rely on the things it's testing. If the goal is to test whether a value is set in local storage (or clear the value from local storage), then using the UID2LocalStorageManager to do that is a problem.

For example, with this approach, if I change UID2LocalStorageManager to this:
image

The tests all still pass, even though UID2LocalStorageManager doesn't use localStorage at all and would be horribly broken in a real context (it would never persist any data and always start as null on each page load).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

thanks for the feedback Lionell, I've reverted the change so that I'm using getUid2LocalStorage again. One thing I wanted to check with you again @lionell-pack-ttd - are you okay with the implementation of getUid2LocalStorage?

Specifically, the : undefined is necessary because the mock method getUid2Cookie returns undefined, and we have tests that run for both local storage and cookie that assert on toBeUndefined().

Other alternatives includes:

  • Changing getUid2Cookie to return null if !docCookie, then change getUid2LocalStorage to return null instead of undefined
  • Changing the assertions to be toBeFalsy() instead of toBeUndefined(). The issue with this is that it also includes false, 0 and '' which I don't particularly like - as far as I can tell that is the only way to cover toBeNull() || toBeUndefined()

I'd lean towards the first alternative unless you feel strongly about it.

Copy link
Contributor

Choose a reason for hiding this comment

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

The first approach seems like a good one to me - but I don't really feel strongly here, whichever you think is best.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've gone with this approach:

Changing getUid2Cookie to return null if !docCookie, then change getUid2LocalStorage to return null instead of undefined

src/uid2Sdk.ts Outdated
@@ -17,6 +17,8 @@ import {
isClientSideIdentityOptionsOrThrow,
} from "./uid2ClientSideIdentityOptions";
import { bytesToBase64 } from "./uid2Base64";
import { UID2LocalStorageManager } from "./uid2LocalStorageManager";
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: this import is never used

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed!

src/uid2StorageManager.ts Outdated Show resolved Hide resolved
@@ -0,0 +1,47 @@
import { UID2 } from "./uid2Sdk";
import { isValidIdentity, Uid2Identity } from "./Uid2Identity";
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: isValidIdentity and UID2 is not used

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed!

src/uid2StorageManager.ts Outdated Show resolved Hide resolved
this._localStorageManager = new UID2LocalStorageManager();
}

public loadIdentityFromPreferredStorageWithFallback(): Uid2Identity | null {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This probably deserves some tests

- Clean up imports
- Rename usage of loadIdentityFromPreferredStorageWithFallback
- Return early in `setValue` method

testCookieAndLocalStorage(() => {
describe("when getAdvertisingTokenAsync is called before init", () => {
describe("when initialising with a valid identity", () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we cover test cases for when initialising without identity? (which calls the loadIdentityWithFallback?)

Copy link
Contributor

Choose a reason for hiding this comment

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

Do we have any test that testing if the refreshed token has been stored to the correct storage?

});
const p = uid2.getAdvertisingTokenAsync();
uid2.init({ identity: originalIdentity, useCookie: useCookie });
xhrMock.responseText = btoa(
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: maybe we could wrap xhrMock.responseText and xhrMock.onreadystatechange into a mockResponse(response) function?

- Use localStorageManager methods in mocks or in tests directly
- Remove ununsed imports
import { Uid2Options } from "./Uid2Options";

export class UID2StorageManager {
private _cookieManager: UID2CookieManager | undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

I dropped you a DM about this - I think the coverage is showing as low on this file because of these | undefined, and these values will always be set once the constructor has run.

Drop | undefined from managers inside UID2StorageManager and associted null coalescing. Was not necessary and gave confusing code coverage insights
@alex-yau-ttd alex-yau-ttd merged commit be8ebf4 into main Oct 9, 2023
@alex-yau-ttd alex-yau-ttd deleted the ajy-UID2-1667-Local-storage-for-v3-js-sdk branch October 9, 2023 04:17
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