PostHog / posthog-js-lite

Reimplementation of posthog-js to be as light and modular as possible.
https://posthog.com/docs/libraries
MIT License
70 stars 36 forks source link

Improve/add support for sync providers, ie rn-mmkv #96

Closed filipef101 closed 8 months ago

filipef101 commented 1 year ago

Is your feature request related to a problem?

Would be nice if we could use MMKV instead of async storage

Describe the solution you'd like

Describe alternatives you've considered

Related sub-libraries

Additional context

Thank you for your feature request – we love each and every one!

filipef101 commented 1 year ago

Some people may have mmkv instead of async storage, posthog could detect if async storage not present fallback to mmkv + option to prefer mmkv

benjackwhite commented 1 year ago

We didn't want to end up with 100s of if statements trying to cover every possible dependency 😅 But you can just configure your own interface like so:

const options: PostHogOptions = {
  customAsyncStorage: {
    getItem: (key: string) => Promise<string | null>,
    setItem: (key: string, value: string) => Promise<void>
  }
}

And then you can use whatever storage you like :)

filipef101 commented 1 year ago

mmkv is sync, any chance to get a sync storage provider? to be able to have feature flags on first render/paint @benjackwhite

benjackwhite commented 1 year ago

I'm not 100% certain but I would expect if you simply resolve immediately in those custom functions then it basically will be sync, especially from a first-render point of view?

marandaneto commented 11 months ago

The bridge is async by design, so even if you resolve/await a function, it does not guarantee the order of execution, it might be that something is executed before your awaited function (it's an event loop/single thread), that has also been awaited for.

A solution for all the async problems is to migrate to the RN new architecture and define when a method is async/sync or even have both. Since all the storage plugins are Native libs (android/ios), this would be the best/most reliable option.

For most cases, just awaiting the methods is enough, showing loading progress. At the same time, the calls get resolved, and after you decide what to do depending on the resolved feature flags, it's not necessary to be before the first render/paint, you can work around that by just using this suggestion for now.

filipef101 commented 11 months ago

@marandaneto I don't see any native code, so what needs to be migrated or am i missing something?

filipef101 commented 11 months ago

I'm not 100% certain but I would expect if you simply resolve immediately in those custom functions then it basically will be sync, especially from a first-render point of view?

@benjackwhite doesnt matter how fast it is, if you need to await it wont be available on first paint/render

marandaneto commented 11 months ago

@marandaneto I don't see any native code, so what needs to be migrated or am i missing something?

Right now we try to use expo-file-system or AsyncStorage, both are async by design.

We'd need to either change SemiAsyncStorage or create an SyncStorage type where all methods are sync. including the preloadAsync. Also create a customSyncStorage instead of customAsyncStorage, etc.

Also, use another file system plugin that supports the new architecture/JSI with only sync. methods, for example, react-native-mmkv.

So it's a bit of refactoring on the TS code actually, not native code, plus supporting react-native-mmkv.

filipef101 commented 9 months ago

Well, I am now about to use the feature flags feature, and I guess, this is not such an annoying issue?

The SemiAsync implementation is actually sync, besides the preload async. So if I write my own sync storage, I can follow the same API, and that function can be empty 🪄 💫 the only downside is that I stil have an unnecessaryly async "initAsync"

filipef101 commented 9 months ago

If any change gets planned, I'd make the posthog-rn.ts only have a storage implementation/declared, removing checks for "memory or semiAsyncStorage". making it configurable by passing 'async' | 'memory' or 'custom' or some object, or whatever web might also do for custom providers. Move "workarounds" to the storage.ts implementation i.e., the "removeItem" check for nulls here

Makes little sense that I need to call initAsync if I'm using the memory provider, would also not make sense for a sync provider, so allowing for either initAsync or initSync to exist or make any "sync" setup part of the constructor when using sync providers, like memory, or JSI implementations like rn-mmkv

Making the implementation of the setupAsync optional in the SemiAsyncStorage type/class makes it perfectly suitable for both sync and async custom providers implementations :) (right now I'll write my own with that empty)

Maybe getting out of topic, but I don't understand the existence/use of the methods getAllKeys and clear on the SemiAsyncStorage. There might be others, similarly the web cookieStore also implements it but there is no usage. Tech debt/legacy?

filipef101 commented 9 months ago

This can be done with couple small changes:

marandaneto commented 9 months ago

@filipef101 would you like to submit a draft PR with your suggestion and we take it from there? I am all in to guide/review your suggestions to make it better. I'd just try to keep compatibility with previous versions if possible.

filipef101 commented 9 months ago

Yes, already started!

filipef101 commented 9 months ago

@marandaneto are you able to take it from #170 ? Cheers

marandaneto commented 9 months ago

@filipef101 can you check if https://github.com/PostHog/posthog-js-lite/pull/189/ helps with your use case? It's not released yet though.

filipef101 commented 9 months ago

@marandaneto Step in the right direction, not there yet? Still will need to create a semiAsyncStorage with empty/immediate resolve preloadAsync to use mmkv as storage.

I feel that it is wrongly exposing a sync functionality, but in practice there will be a small delay before it is actually initialized, so for example using feature flags for conditional UI may "flicker" as first render a feature flag that we could expect to be cached is still undefined.

What happens if I try to log an event, or even access a feature flag, on first render? Will the event sending be "delayed" until the _isInitialized is true? or will it fail if anything happens before _isInitialized becomes true?

filipef101 commented 9 months ago

I'd be okay with closing. The point still stands as a very likely use case of using PH feature flags for conditional rendering on first paint, currently there is atleast 1 first render where it will always be undefined.

My current solution is to just use pg as is, and wrap the useFeatureFlag, in it's own hook that caches the flag with mmkv, ie when undefined return from cache if exists. Ideally I would want to not use async storage as I

(I also use it to nicely get the payload if exists instead of only the boolean value as afaik theres no useFFPayload hook)

marandaneto commented 8 months ago

If you init PostHog with a custom customAsyncStorage (mmkv for example), _semiAsyncStorage is created but resolved async, indeed.

A call to isFeatureEnabled or similar will return undefined if not loaded yet, correct.

All calls to capture will be executed after the _initPromise is resolved if not yet, otherwise it's all sync calls (so just a bit of delay - probably a few ms if any).

You can call and await posthog.ready() on a loading state or something if you wanna be sure everything is loaded.

I get your point, ideally if a sync storage is provided flags should not have returned undefined nor you need to call ready() on the first render.

benjackwhite commented 8 months ago

@filipef101 would be keen to hear your input as someone with knowledge of rn-mmkv - Does it make sense to try and offer it as a built in option via peer-dependencies like we do with AsyncStorage?

I ask as from the docs it seems that there is a bit more potential config desired with mmkv in terms of scoping and namespacing it so it might not make sense to offer as a default but rather support via the new config option. Thoughts?

marandaneto commented 8 months ago

Closing this issue as resolved since it's possible to pass custom storage, we can raise a new issue based on the answer here.

Thank you all.

filipef101 commented 8 months ago

@marandaneto Thanks for considering the request and adding support for my use case. And all the time taken considering my suggestions and feedback. Very much appreciated. Rockstar work 🤘💫

filipef101 commented 8 months ago

@filipef101 would be keen to hear your input as someone with knowledge of rn-mmkv - Does it make sense to try and offer it as a built in option via peer-dependencies like we do with AsyncStorage?

I ask as from the docs it seems that there is a bit more potential config desired with mmkv in terms of scoping and namespacing it so it might not make sense to offer as a default but rather support via the new config option. Thoughts?

Yeah think it would make sense, nowadays RN has no async storage by default so it may become more common that people want to use it

I would not default to it if async storage is not found. Unless it was clear for users that they needed to migrate from async to mmkv. (That is also why i didnt ask for mmkv support on ph side as that introduces a whole new problem :) )

filipef101 commented 8 months ago

@marandaneto / @benjackwhite FYI a bunch of docs might need to be updated to use the new sync api ie:https://posthog.com/docs/feature-flags/installation?tab=React+Native

marandaneto commented 8 months ago

https://github.com/PostHog/posthog/pull/20854 https://github.com/PostHog/posthog.com/pull/8021 https://github.com/PostHog/posthog.com/pull/7902 We'll merge once its GA, let's give a few more days after the last beta with a few fixes.

bob-devereux commented 7 months ago

Is this working? If so, is there any documentation on how to configure postHog for MMKV?

marandaneto commented 7 months ago

Is this working? If so, is there any documentation on how to configure postHog for MMKV?

Good point, https://github.com/PostHog/posthog.com/pull/8232 Just install the MMKV lib and create your own customStorage. Be aware that if you were using a previous storage form, the data won't be migrated so cached events, cached user id, etc won't be available. I recommend calling identify right away after the new storage.