Skip to content

Feature Request: Support custom storage implementation for identityStorage option #1061

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

Open
andrewshvv opened this issue May 4, 2025 · 2 comments
Labels
enhancement New feature or request

Comments

@andrewshvv
Copy link

andrewshvv commented May 4, 2025

Summary

Allow passing custom storage implementation to identityStorage option instead of only supporting predefined types:

  • cookie
  • localStorage
  • sessionStorage
  • none
// Proposed API
amplitude.init(API_KEY, undefined, {
  identityStorage: new MyCustomStorageImplementation() // Custom implementation of Storage<UserSession>
});

Motivations

Consistent session tracking between background script, sidepanel, and website is critical for accurate analytics. Sessions currently break across these contexts because Amplitude doesn't support Chrome's extension-specific storage APIs.

Background scripts, sidepanel, and website can all access shared storage through Chrome's extension APIs (chrome.cookies, etc.), but Amplitude only supports standard web storage mechanisms which don't work across extension contexts.

Allowing custom storage implementations would enable developers to implement consistent session tracking using Chrome's extension APIs without requiring Amplitude to include extension-specific dependencies in their core package.

@andrewshvv andrewshvv added the enhancement New feature or request label May 4, 2025
@andrewshvv
Copy link
Author

andrewshvv commented May 4, 2025

Another weird approach for a future reference:

import {browser} from 'wxt/browser';

interface StorageSubstitute {
    getItem(key: string): Promise<string | null>;
    setItem(key: string, value: string): Promise<void>;
    removeItem(key: string): Promise<void>;
    clear(): Promise<void>;
}

interface CookieStorageOptions {
    domain?: string;
    expirationDays?: number;
    sameSite?: string;
    secure?: boolean;
}

export class CookieStorage implements StorageSubstitute {
    private options: CookieStorageOptions;
    private websiteUrl: string;

    constructor(websiteUrl: string, options?: CookieStorageOptions) {
        this.options = {...options};
        this.websiteUrl = import.meta.env.WXT_MATHAI_WEBSITE;
    }

    async getItem(key: string): Promise<string | null> {
        try {
            const cookie = await browser.cookies.get({
                url: this.websiteUrl,
                name: key
            });

            if (!cookie?.value) {
                return null;
            }

            try {
                const decodedValue = decodeURIComponent(atob(cookie.value));
                return decodedValue;
            } catch {
                try {
                    const decodedValue = decodeURIComponent(atob(decodeURIComponent(cookie.value)));
                    return decodedValue;
                } catch (error) {
                    console.error(`[CookieStorage] Failed to decode cookie value for key: ${key}, value: ${cookie.value}`);
                    return null;
                }
            }
        } catch (error) {
            console.error(`[CookieStorage] Error getting "${key}":`, error);
            return null;
        }
    }

    async setItem(key: string, value: string): Promise<void> {
        try {
            if (value === null || value === undefined) {
                await this.removeItem(key);
                return;
            }

            const expirationDays = this.options.expirationDays ?? 0;
            let expirationDate: number | undefined = undefined;

            if (expirationDays) {
                expirationDate = Math.floor(Date.now() / 1000) + (expirationDays * 24 * 60 * 60);
            }

            let domain = this.options.domain;
            if (!domain && this.websiteUrl) {
                try {
                    const url = new URL(this.websiteUrl);
                    domain = url.hostname;
                } catch (error) {
                    console.error('[CookieStorage] Invalid URL:', this.websiteUrl);
                }
            }

            const encodedValue = btoa(encodeURIComponent(value));

            await browser.cookies.set({
                url: this.websiteUrl,
                name: key,
                value: encodedValue,
                domain: domain,
                path: '/',
                secure: this.options.secure ?? this.websiteUrl.startsWith('https://'),
                httpOnly: false,
                expirationDate: expirationDate,
                sameSite: this.options.sameSite as any || 'lax'
            });
        } catch (error) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            console.error(`[CookieStorage] Failed to set cookie for key: ${key}. Error: ${errorMessage}`);
        }
    }

    async removeItem(key: string): Promise<void> {
        try {
            await browser.cookies.remove({
                url: this.websiteUrl,
                name: key
            });
        } catch (error) {
            console.error(`[CookieStorage] Error removing "${key}":`, error);
        }
    }

    async clear(): Promise<void> {
        return Promise.resolve();
    }
}

let ourContext = {
    localStorage: new CookieStorage(import.meta.env.WXT_MATHAI_WEBSITE, {
        expirationDays: 30,
        secure: true
    }),
};

const contextProxy = new Proxy(ourContext, {
    get(target: any, prop: string | symbol, receiver: any): any {
        if (prop in target) {
            return target[prop];
        }

        const value = (globalThis as any)[prop];

        if (typeof value === 'function') {
            return function (...args: any[]): any {
                return value.apply(globalThis, args);
            };
        }

        return value;
    }
});

globalThis.ampIntegrationContext = contextProxy;
amplitude.init(import.meta.env.WXT_AMPLITUDE_API_KEY, {
        // WARNING: We actually use our own storage.
        identityStorage: "localStorage",
})

@daniel-graham-amplitude
Copy link
Collaborator

Thanks for the request. I'll add this to our backlog, but of course I can't give any promises on timelines.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants