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

chore: ref to allow sync storage friendlier #198

Merged
merged 15 commits into from
Mar 4, 2024
Next Next commit
chore: ref to allow sync storage friendlier
marandaneto committed Mar 1, 2024
commit 471f76b2373435f58a18e66a3aa1b9c6041a9fa9
11 changes: 10 additions & 1 deletion posthog-react-native/src/native-deps.tsx
Original file line number Diff line number Diff line change
@@ -81,11 +81,20 @@ export const buildOptimisiticAsyncStorage = (): PostHogCustomAsyncStorage => {
const uri = (filesystem.documentDirectory || '') + key
await filesystem.writeAsStringAsync(uri, value)
},

isSemiAsync(): boolean {
return true
},
}
}

if (OptionalAsyncStorage) {
return OptionalAsyncStorage
return {
...OptionalAsyncStorage,
isSemiAsync(): boolean {
return true;
},
};
}

throw new Error(
40 changes: 25 additions & 15 deletions posthog-react-native/src/posthog-rn.ts
Original file line number Diff line number Diff line change
@@ -10,26 +10,27 @@
} from '../../posthog-core/src'
import { PostHogMemoryStorage } from '../../posthog-core/src/storage-memory'
import { getLegacyValues } from './legacy'
import { SemiAsyncStorage } from './storage'
import { SemiAsyncStorage, Storage, SyncStorage } from './storage'
import { version } from './version'
import { buildOptimisiticAsyncStorage, getAppProperties } from './native-deps'
import { PostHogAutocaptureOptions, PostHogCustomAppProperties, PostHogCustomAsyncStorage } from './types'
import { PostHogAutocaptureOptions, PostHogCustomAppProperties, PostHogCustomAsyncStorage, PostHogCustomSyncStorage } from './types'
import { withReactNativeNavigation } from './frameworks/wix-navigation'
import AsyncStorage from '@react-native-async-storage/async-storage'

Check failure on line 18 in posthog-react-native/src/posthog-rn.ts

GitHub Actions / lint

'AsyncStorage' is defined but never used

export type PostHogOptions = PostHogCoreOptions & {
persistence?: 'memory' | 'file'
/** Allows you to provide your own implementation of the common information about your App or a function to modify the default App properties generated */
customAppProperties?:
| PostHogCustomAppProperties
| ((properties: PostHogCustomAppProperties) => PostHogCustomAppProperties)
customAsyncStorage?: PostHogCustomAsyncStorage
customStorage?: PostHogCustomAsyncStorage | PostHogCustomSyncStorage
captureNativeAppLifecycleEvents?: boolean
}

export class PostHog extends PostHogCore {
private _persistence: PostHogOptions['persistence']
private _memoryStorage = new PostHogMemoryStorage()
private _semiAsyncStorage?: SemiAsyncStorage
private _storage?: Storage
private _appProperties: PostHogCustomAppProperties = {}

constructor(apiKey: string, options?: PostHogOptions) {
@@ -48,22 +49,31 @@
})

if (this._persistence === 'file') {
this._semiAsyncStorage = new SemiAsyncStorage(options?.customAsyncStorage || buildOptimisiticAsyncStorage())
const storage = options?.customStorage || buildOptimisiticAsyncStorage()

if (storage.isSemiAsync()) {
const asyncStorage = new SemiAsyncStorage(storage as PostHogCustomAsyncStorage)
this._storage = asyncStorage
} else {
const syncStorage = new SyncStorage(storage as PostHogCustomSyncStorage)
this._storage = syncStorage
syncStorage.preload()
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
}
}

// Ensure the async storage has been preloaded (this call is cached)
const setupAsync = async (): Promise<void> => {
if (!this._semiAsyncStorage?.isPreloaded) {
await this._semiAsyncStorage?.preloadAsync()
if (!this._storage?.isPreloaded && this._storage?.isSemiAsync()) {
await (this._storage as SemiAsyncStorage)?.preloadAsync()
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
}
this.setupBootstrap(options)

// It is possible that the old library was used so we try to get the legacy distinctID
if (!this._semiAsyncStorage?.getItem(PostHogPersistedProperty.AnonymousId)) {
if (!this._storage?.getItem(PostHogPersistedProperty.AnonymousId)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Technically none of this applies if the storage isn't async - the only reason we do things the way we do with async storage is to have allowed people to move from the old native SDKs.

const legacyValues = await getLegacyValues()
if (legacyValues?.distinctId) {
this._semiAsyncStorage?.setItem(PostHogPersistedProperty.DistinctId, legacyValues.distinctId)
this._semiAsyncStorage?.setItem(PostHogPersistedProperty.AnonymousId, legacyValues.anonymousId)
this._storage?.setItem(PostHogPersistedProperty.DistinctId, legacyValues.distinctId)
this._storage?.setItem(PostHogPersistedProperty.AnonymousId, legacyValues.anonymousId)
}
}
}
@@ -97,15 +107,15 @@
}

getPersistedProperty<T>(key: PostHogPersistedProperty): T | undefined {
return this._semiAsyncStorage
? this._semiAsyncStorage.getItem(key) ?? undefined
return this._storage
? this._storage.getItem(key) ?? undefined
: this._memoryStorage.getProperty(key)
}
setPersistedProperty<T>(key: PostHogPersistedProperty, value: T | null): void {
return this._semiAsyncStorage
return this._storage
? value !== null
? this._semiAsyncStorage.setItem(key, value)
: this._semiAsyncStorage.removeItem(key)
? this._storage.setItem(key, value)
: this._storage.removeItem(key)
: this._memoryStorage.setProperty(key, value)
}

128 changes: 81 additions & 47 deletions posthog-react-native/src/storage.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,79 @@
import { PostHogCustomAsyncStorage } from './types'
import { PostHogCustomAsyncStorage, PostHogCustomSyncStorage } from './types'

const POSTHOG_STORAGE_KEY = '.posthog-rn.json'
const POSTHOG_STORAGE_VERSION = 'v1'

type PostHogStorageContents = { [key: string]: any }

export class Storage {
memoryCache: PostHogStorageContents = {}
storage: PostHogCustomAsyncStorage | PostHogCustomSyncStorage
isPreloaded = false

constructor(storage: PostHogCustomAsyncStorage | PostHogCustomSyncStorage) {
this.storage = storage
}

isSemiAsync(): boolean {
return this.storage.isSemiAsync()
}

persist(): void {
const payload = {
version: POSTHOG_STORAGE_VERSION,
content: this.memoryCache,
}

void this.storage.setItem(POSTHOG_STORAGE_KEY, JSON.stringify(payload))
}

getItem(key: string): any | null | undefined {
return this.memoryCache[key]
}
setItem(key: string, value: any): void {
this.memoryCache[key] = value
this.persist()
}
removeItem(key: string): void {
delete this.memoryCache[key]
this.persist()
}
clear(): void {
for (const key in this.memoryCache) {
delete this.memoryCache[key]
}
this.persist()
}
getAllKeys(): readonly string[] {
return Object.keys(this.memoryCache)
}

populateMemoryCache(res: string | null): void {
try {
const data = res ? JSON.parse(res).content : {}

for (const key in data) {
this.memoryCache[key] = data[key]
}
} catch (e) {
console.warn(
"PostHog failed to load persisted data from storage. This is likely because the storage format is. We'll reset the storage.",
e
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
)
} finally {
this.isPreloaded = true
}
}
}

// NOTE: The core prefers a synchronous storage so we mimic this by pre-loading all keys
export class SemiAsyncStorage {
private _memoryCache: PostHogStorageContents = {}
export class SemiAsyncStorage extends Storage {
private _preloadSemiAsyncStoragePromise: Promise<void> | undefined
private _asyncStorage: PostHogCustomAsyncStorage
public isPreloaded = false
private _storage: PostHogCustomAsyncStorage

constructor(asyncStorage: PostHogCustomAsyncStorage) {
this._asyncStorage = asyncStorage
constructor(storage: PostHogCustomAsyncStorage) {
super(storage)
this._storage = storage
}

preloadAsync(): Promise<void> {
@@ -25,21 +85,8 @@ export class SemiAsyncStorage {
return this._preloadSemiAsyncStoragePromise
}

this._preloadSemiAsyncStoragePromise = this._asyncStorage.getItem(POSTHOG_STORAGE_KEY).then((res) => {
try {
const data = res ? JSON.parse(res).content : {}

for (const key in data) {
this._memoryCache[key] = data[key]
}
} catch (e) {
console.warn(
"PostHog failed to load persisted data from storage. This is likely because the storage format is. We'll reset the storage.",
e
)
} finally {
this.isPreloaded = true
}
this._preloadSemiAsyncStoragePromise = this._storage.getItem(POSTHOG_STORAGE_KEY).then((res) => {
this.populateMemoryCache(res)
})

this._preloadSemiAsyncStoragePromise.finally(() => {
@@ -48,34 +95,21 @@ export class SemiAsyncStorage {

return this._preloadSemiAsyncStoragePromise
}
}

persist(): void {
const payload = {
version: POSTHOG_STORAGE_VERSION,
content: this._memoryCache,
}

void this._asyncStorage.setItem(POSTHOG_STORAGE_KEY, JSON.stringify(payload))
export class SyncStorage extends Storage {
private _storage: PostHogCustomSyncStorage
constructor(storage: PostHogCustomSyncStorage) {
super(storage)
this._storage = storage
}

getItem(key: string): any | null | undefined {
return this._memoryCache[key]
}
setItem(key: string, value: any): void {
this._memoryCache[key] = value
this.persist()
}
removeItem(key: string): void {
delete this._memoryCache[key]
this.persist()
}
clear(): void {
for (const key in this._memoryCache) {
delete this._memoryCache[key]
preload(): void {
if (this.isPreloaded) {
return
}
this.persist()
}
getAllKeys(): readonly string[] {
return Object.keys(this._memoryCache)

const res = this._storage.getItem(POSTHOG_STORAGE_KEY)
this.populateMemoryCache(res)
}
}
13 changes: 10 additions & 3 deletions posthog-react-native/src/types.ts
Original file line number Diff line number Diff line change
@@ -47,7 +47,14 @@ export interface PostHogCustomAppProperties {
$timezone?: string | null
}

export interface PostHogCustomAsyncStorage {
getItem: (key: string) => Promise<string | null>
setItem: (key: string, value: string) => Promise<void>
export abstract class PostHogCustomAsyncStorage {
abstract getItem: (key: string) => Promise<string | null>
abstract setItem: (key: string, value: string) => Promise<void>
abstract isSemiAsync(): boolean
}

export abstract class PostHogCustomSyncStorage {
abstract getItem: (key: string) => string | null
abstract setItem: (key: string, value: string) => void
abstract isSemiAsync(): boolean
}