diff options
| author | MaheshtheDev <[email protected]> | 2025-09-08 05:19:10 +0000 |
|---|---|---|
| committer | MaheshtheDev <[email protected]> | 2025-09-08 05:19:10 +0000 |
| commit | 3e5ea2fd9ed210644ae29b013b579703e30986de (patch) | |
| tree | 7de64cf7bddca72cf56e89ee4320005dad9f2ac5 /apps/browser-extension/utils | |
| parent | fix: billing page (#416) (diff) | |
| download | supermemory-3e5ea2fd9ed210644ae29b013b579703e30986de.tar.xz supermemory-3e5ea2fd9ed210644ae29b013b579703e30986de.zip | |
extension: updated telemetry and batch upload (#415)
Diffstat (limited to 'apps/browser-extension/utils')
| -rw-r--r-- | apps/browser-extension/utils/api.ts | 92 | ||||
| -rw-r--r-- | apps/browser-extension/utils/constants.ts | 15 | ||||
| -rw-r--r-- | apps/browser-extension/utils/posthog.ts | 74 | ||||
| -rw-r--r-- | apps/browser-extension/utils/twitter-import.ts | 77 | ||||
| -rw-r--r-- | apps/browser-extension/utils/types.ts | 4 | ||||
| -rw-r--r-- | apps/browser-extension/utils/ui-components.ts | 149 |
6 files changed, 220 insertions, 191 deletions
diff --git a/apps/browser-extension/utils/api.ts b/apps/browser-extension/utils/api.ts index 2b95c6e2..7e4de310 100644 --- a/apps/browser-extension/utils/api.ts +++ b/apps/browser-extension/utils/api.ts @@ -1,27 +1,27 @@ /** * API service for supermemory browser extension */ -import { API_ENDPOINTS, STORAGE_KEYS } from "./constants"; +import { API_ENDPOINTS, STORAGE_KEYS } from "./constants" import { AuthenticationError, type MemoryPayload, type Project, type ProjectsResponse, SupermemoryAPIError, -} from "./types"; +} from "./types" /** * Get bearer token from storage */ async function getBearerToken(): Promise<string> { - const result = await chrome.storage.local.get([STORAGE_KEYS.BEARER_TOKEN]); - const token = result[STORAGE_KEYS.BEARER_TOKEN]; + const result = await chrome.storage.local.get([STORAGE_KEYS.BEARER_TOKEN]) + const token = result[STORAGE_KEYS.BEARER_TOKEN] if (!token) { - throw new AuthenticationError("Bearer token not found"); + throw new AuthenticationError("Bearer token not found") } - return token; + return token } /** @@ -31,7 +31,7 @@ async function makeAuthenticatedRequest<T>( endpoint: string, options: RequestInit = {}, ): Promise<T> { - const token = await getBearerToken(); + const token = await getBearerToken() const response = await fetch(`${API_ENDPOINTS.SUPERMEMORY_API}${endpoint}`, { ...options, @@ -41,19 +41,19 @@ async function makeAuthenticatedRequest<T>( "Content-Type": "application/json", ...options.headers, }, - }); + }) if (!response.ok) { if (response.status === 401) { - throw new AuthenticationError("Invalid or expired token"); + throw new AuthenticationError("Invalid or expired token") } throw new SupermemoryAPIError( `API request failed: ${response.statusText}`, response.status, - ); + ) } - return response.json(); + return response.json() } /** @@ -62,11 +62,11 @@ async function makeAuthenticatedRequest<T>( export async function fetchProjects(): Promise<Project[]> { try { const response = - await makeAuthenticatedRequest<ProjectsResponse>("/v3/projects"); - return response.projects; + await makeAuthenticatedRequest<ProjectsResponse>("/v3/projects") + return response.projects } catch (error) { - console.error("Failed to fetch projects:", error); - throw error; + console.error("Failed to fetch projects:", error) + throw error } } @@ -77,11 +77,11 @@ export async function getDefaultProject(): Promise<Project | null> { try { const result = await chrome.storage.local.get([ STORAGE_KEYS.DEFAULT_PROJECT, - ]); - return result[STORAGE_KEYS.DEFAULT_PROJECT] || null; + ]) + return result[STORAGE_KEYS.DEFAULT_PROJECT] || null } catch (error) { - console.error("Failed to get default project:", error); - return null; + console.error("Failed to get default project:", error) + return null } } @@ -92,10 +92,10 @@ export async function setDefaultProject(project: Project): Promise<void> { try { await chrome.storage.local.set({ [STORAGE_KEYS.DEFAULT_PROJECT]: project, - }); + }) } catch (error) { - console.error("Failed to set default project:", error); - throw error; + console.error("Failed to set default project:", error) + throw error } } @@ -107,11 +107,11 @@ export async function saveMemory(payload: MemoryPayload): Promise<unknown> { const response = await makeAuthenticatedRequest<unknown>("/v3/memories", { method: "POST", body: JSON.stringify(payload), - }); - return response; + }) + return response } catch (error) { - console.error("Failed to save memory:", error); - throw error; + console.error("Failed to save memory:", error) + throw error } } @@ -123,34 +123,40 @@ export async function searchMemories(query: string): Promise<unknown> { const response = await makeAuthenticatedRequest<unknown>("/v4/search", { method: "POST", body: JSON.stringify({ q: query, include: { relatedMemories: true } }), - }); - return response; + }) + return response } catch (error) { - console.error("Failed to search memories:", error); - throw error; + console.error("Failed to search memories:", error) + throw error } } /** * Save tweet to Supermemory API (specific for Twitter imports) */ -export async function saveTweet( - content: string, - metadata: { sm_source: string; [key: string]: unknown }, - containerTag = "sm_project_twitter_bookmarks", -): Promise<void> { +export async function saveAllTweets( + documents: MemoryPayload[], +): Promise<unknown> { try { - const payload: MemoryPayload = { - containerTags: [containerTag], - content, - metadata, - }; - await saveMemory(payload); + const response = await makeAuthenticatedRequest<unknown>( + "/v3/memories/batch", + { + method: "POST", + body: JSON.stringify({ + documents, + metadata: { + sm_source: "consumer", + sm_internal_group_id: "twitter_bookmarks", + }, + }), + }, + ) + return response } catch (error) { if (error instanceof SupermemoryAPIError && error.statusCode === 409) { // Skip if already exists (409 Conflict) - return; + return } - throw error; + throw error } } diff --git a/apps/browser-extension/utils/constants.ts b/apps/browser-extension/utils/constants.ts index 7634759b..5ebd76d1 100644 --- a/apps/browser-extension/utils/constants.ts +++ b/apps/browser-extension/utils/constants.ts @@ -15,6 +15,7 @@ export const API_ENDPOINTS = { */ export const STORAGE_KEYS = { BEARER_TOKEN: "bearer-token", + USER_DATA: "user-data", TOKENS_LOGGED: "tokens-logged", TWITTER_COOKIE: "twitter-cookie", TWITTER_CSRF: "twitter-csrf", @@ -27,10 +28,6 @@ export const STORAGE_KEYS = { */ export const ELEMENT_IDS = { TWITTER_IMPORT_BUTTON: "sm-twitter-import-button", - TWITTER_IMPORT_STATUS: "sm-twitter-import-status", - TWITTER_CLOSE_BTN: "sm-twitter-close-btn", - TWITTER_IMPORT_BTN: "sm-twitter-import-btn", - TWITTER_SIGNIN_BTN: "sm-twitter-signin-btn", SUPERMEMORY_TOAST: "sm-toast", SUPERMEMORY_SAVE_BUTTON: "sm-save-button", SAVE_TWEET_ELEMENT: "sm-save-tweet-element", @@ -83,3 +80,13 @@ export const MESSAGE_TYPES = { export const CONTEXT_MENU_IDS = { SAVE_TO_SUPERMEMORY: "sm-save-to-supermemory", } as const + +export const POSTHOG_EVENT_KEY = { + TWITTER_IMPORT_STARTED: "twitter_import_started", + SAVE_MEMORY_ATTEMPTED: "save_memory_attempted", + SAVE_MEMORY_ATTEMPT_FAILED: "save_memory_attempt_failed", + SOURCE: "extension", + T3_CHAT_MEMORIES_SEARCHED: "t3_chat_memories_searched", + CLAUDE_CHAT_MEMORIES_SEARCHED: "claude_chat_memories_searched", + CHATGPT_CHAT_MEMORIES_SEARCHED: "chatgpt_chat_memories_searched", +} as const diff --git a/apps/browser-extension/utils/posthog.ts b/apps/browser-extension/utils/posthog.ts new file mode 100644 index 00000000..cdcdbc4e --- /dev/null +++ b/apps/browser-extension/utils/posthog.ts @@ -0,0 +1,74 @@ +import { PostHog } from "posthog-js/dist/module.no-external" +import { STORAGE_KEYS } from "./constants" + +export async function identifyUser(posthog: PostHog): Promise<void> { + const stored = await chrome.storage.local.get([STORAGE_KEYS.USER_DATA]) + const userData = stored[STORAGE_KEYS.USER_DATA] + + if (userData?.userId) { + posthog.identify(userData.userId, { + email: userData.email, + name: userData.name, + userId: userData.userId, + }) + } +} + +let posthogInstance: PostHog | null = null +let initializationPromise: Promise<PostHog> | null = null + +export const POSTHOG_CONFIG = { + api_host: "https://api.supermemory.ai/orange", + person_profiles: "identified_only", + disable_external_dependency_loading: true, + persistence: "localStorage", + capture_pageview: false, + autocapture: false, +} as const + +export async function getPostHogInstance(): Promise<PostHog> { + if (posthogInstance) { + return posthogInstance + } + + if (initializationPromise) { + return initializationPromise + } + + initializationPromise = initializePostHog() + return initializationPromise +} + +async function initializePostHog(): Promise<PostHog> { + try { + const posthog = new PostHog() + + if (!import.meta.env.WXT_POSTHOG_API_KEY) { + console.error("PostHog API key not configured") + throw new Error("PostHog API key not configured") + } + + posthog.init(import.meta.env.WXT_POSTHOG_API_KEY || "", POSTHOG_CONFIG) + + await identifyUser(posthog) + + posthogInstance = posthog + return posthog + } catch (error) { + console.error("Failed to initialize PostHog:", error) + initializationPromise = null + throw error + } +} + +export async function trackEvent( + eventName: string, + properties?: Record<string, unknown>, +): Promise<void> { + try { + const posthog = await getPostHogInstance() + posthog.capture(eventName, properties) + } catch (error) { + console.error(`Failed to track event ${eventName}:`, error) + } +} diff --git a/apps/browser-extension/utils/twitter-import.ts b/apps/browser-extension/utils/twitter-import.ts index c516e094..e68d6dbf 100644 --- a/apps/browser-extension/utils/twitter-import.ts +++ b/apps/browser-extension/utils/twitter-import.ts @@ -3,16 +3,14 @@ * Handles the import process for Twitter bookmarks */ -import { saveTweet } from "./api" +import { saveAllTweets } from "./api" import { createTwitterAPIHeaders, getTwitterTokens } from "./twitter-auth" import { BOOKMARKS_URL, buildRequestVariables, extractNextCursor, getAllTweets, - type Tweet, type TwitterAPIResponse, - tweetToMarkdown, } from "./twitter-utils" export type ImportProgressCallback = (message: string) => Promise<void> @@ -48,31 +46,6 @@ class RateLimiter { } /** - * Imports a single tweet to Supermemory - * @param tweetMd - Tweet content in markdown format - * @param tweet - Original tweet object with metadata - * @returns Promise that resolves when tweet is imported - */ -async function importTweet(tweetMd: string, tweet: Tweet): Promise<void> { - const metadata = { - sm_source: "consumer", - tweet_id: tweet.id_str, - author: tweet.user.screen_name, - created_at: tweet.created_at, - likes: tweet.favorite_count, - retweets: tweet.retweet_count || 0, - } - - try { - await saveTweet(tweetMd, metadata) - } catch (error) { - throw new Error( - `Failed to save tweet: ${error instanceof Error ? error.message : "Unknown error"}`, - ) - } -} - -/** * Main class for handling Twitter bookmarks import */ export class TwitterImporter { @@ -91,9 +64,10 @@ export class TwitterImporter { } this.importInProgress = true + const uniqueGroupId = crypto.randomUUID() try { - await this.batchImportAll("", 0) + await this.batchImportAll("", 0, uniqueGroupId) this.rateLimiter.reset() } catch (error) { await this.config.onError(error as Error) @@ -107,7 +81,7 @@ export class TwitterImporter { * @param cursor - Pagination cursor for Twitter API * @param totalImported - Number of tweets imported so far */ - private async batchImportAll(cursor = "", totalImported = 0): Promise<void> { + private async batchImportAll(cursor = "", totalImported = 0, uniqueGroupId = "twitter_bookmarks"): Promise<void> { try { // Use a local variable to track imported count let importedCount = totalImported @@ -130,9 +104,6 @@ export class TwitterImporter { ? `${BOOKMARKS_URL}&variables=${encodeURIComponent(JSON.stringify(variables))}` : BOOKMARKS_URL - console.log("Making Twitter API request to:", urlWithCursor) - console.log("Request headers:", Object.fromEntries(headers.entries())) - const response = await fetch(urlWithCursor, { method: "GET", headers, @@ -145,7 +116,7 @@ export class TwitterImporter { if (response.status === 429) { await this.rateLimiter.handleRateLimit(this.config.onProgress) - return this.batchImportAll(cursor, totalImported) + return this.batchImportAll(cursor, totalImported, uniqueGroupId) } throw new Error( `Failed to fetch data: ${response.status} - ${errorText}`, @@ -155,21 +126,45 @@ export class TwitterImporter { const data: TwitterAPIResponse = await response.json() const tweets = getAllTweets(data) - console.log("Tweets:", tweets) + const documents: MemoryPayload[] = [] - // Process each tweet + // Convert tweets to MemoryPayload for (const tweet of tweets) { try { - const tweetMd = tweetToMarkdown(tweet) - await importTweet(tweetMd, tweet) + const metadata = { + sm_source: "consumer", + tweet_id: tweet.id_str, + author: tweet.user.screen_name, + created_at: tweet.created_at, + likes: tweet.favorite_count, + retweets: tweet.retweet_count || 0, + sm_internal_group_id: uniqueGroupId, + } + documents.push({ + containerTags: ["sm_project_twitter_bookmarks"], + content: `https://x.com/${tweet.user.screen_name}/status/${tweet.id_str}`, + metadata, + customId: tweet.id_str, + }) importedCount++ - await this.config.onProgress(`Imported ${importedCount} tweets`) + await this.config.onProgress(`Imported ${importedCount} tweets, so far...`) } catch (error) { console.error("Error importing tweet:", error) - // Continue with next tweet } } + try { + if (documents.length > 0) { + await saveAllTweets(documents) + } + console.log("Tweets saved") + console.log("Documents:", documents) + } catch (error) { + console.error("Error saving tweets batch:", error) + await this.config.onError(error as Error) + return + } + // Handle pagination const instructions = data.data?.bookmark_timeline_v2?.timeline?.instructions @@ -180,7 +175,7 @@ export class TwitterImporter { if (nextCursor && tweets.length > 0) { await new Promise((resolve) => setTimeout(resolve, 1000)) // Rate limiting - await this.batchImportAll(nextCursor, importedCount) + await this.batchImportAll(nextCursor, importedCount, uniqueGroupId) } else { await this.config.onComplete(importedCount) } diff --git a/apps/browser-extension/utils/types.ts b/apps/browser-extension/utils/types.ts index 2d0981c8..d20f899e 100644 --- a/apps/browser-extension/utils/types.ts +++ b/apps/browser-extension/utils/types.ts @@ -17,6 +17,7 @@ export interface ExtensionMessage { state?: ToastState importedMessage?: string totalImported?: number + actionSource?: string } /** @@ -32,12 +33,13 @@ export interface MemoryData { * Supermemory API payload for storing memories */ export interface MemoryPayload { - containerTags: string[] + containerTags?: string[] content: string metadata: { sm_source: string [key: string]: unknown } + customId?: string } /** diff --git a/apps/browser-extension/utils/ui-components.ts b/apps/browser-extension/utils/ui-components.ts index 9c060017..29388656 100644 --- a/apps/browser-extension/utils/ui-components.ts +++ b/apps/browser-extension/utils/ui-components.ts @@ -3,7 +3,7 @@ * Reusable UI components for the browser extension */ -import { API_ENDPOINTS, ELEMENT_IDS, UI_CONFIG } from "./constants" +import { ELEMENT_IDS, UI_CONFIG } from "./constants" import type { ToastState } from "./types" /** @@ -158,7 +158,7 @@ export function createTwitterImportButton(onClick: () => void): HTMLElement { color: black; border: none; border-radius: 50px; - padding: 12px 16px; + padding: 10px 12px; cursor: pointer; display: flex; align-items: center; @@ -169,15 +169,16 @@ export function createTwitterImportButton(onClick: () => void): HTMLElement { const iconUrl = browser.runtime.getURL("/icon-16.png") button.innerHTML = ` <img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" /> + <span style="font-weight: 500; font-size: 12px;">Import Bookmarks</span> ` button.addEventListener("mouseenter", () => { - button.style.transform = "scale(1.05)" + button.style.opacity = "0.8" button.style.boxShadow = "0 4px 12px rgba(29, 155, 240, 0.4)" }) button.addEventListener("mouseleave", () => { - button.style.transform = "scale(1)" + button.style.opacity = "1" button.style.boxShadow = "0 2px 8px rgba(29, 155, 240, 0.3)" }) @@ -187,103 +188,6 @@ export function createTwitterImportButton(onClick: () => void): HTMLElement { } /** - * Creates the Twitter import UI dialog - * @param onClose - Close handler - * @param onImport - Import handler - * @param isAuthenticated - Whether user is authenticated - * @returns HTMLElement - The dialog element - */ -export function createTwitterImportUI( - onClose: () => void, - onImport: () => void, - isAuthenticated: boolean, -): HTMLElement { - const container = document.createElement("div") - container.style.cssText = ` - position: fixed; - top: 20px; - right: 20px; - z-index: 2147483647; - background: #ffffff; - border-radius: 12px; - padding: 16px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - min-width: 280px; - max-width: 400px; - border: 1px solid #e1e5e9; - font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - ` - - container.innerHTML = ` - <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;"> - <div style="display: flex; align-items: center; gap: 8px;"> - <svg width="20" height="20" viewBox="0 0 24 24" fill="#1d9bf0"> - <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/> - </svg> - <h3 style="margin: 0; font-size: 16px; font-weight: 600; color: #0f1419;"> - Import Twitter Bookmarks - </h3> - </div> - <button id="${ELEMENT_IDS.TWITTER_CLOSE_BTN}" style="background: none; border: none; cursor: pointer; padding: 4px; border-radius: 4px; color: #536471;"> - ✕ - </button> - </div> - - ${ - isAuthenticated - ? ` - <div> - <p style="color: #536471; font-size: 14px; margin: 0 0 12px 0; line-height: 1.4;"> - This will import all your Twitter bookmarks to Supermemory - </p> - - <button id="${ELEMENT_IDS.TWITTER_IMPORT_BTN}" style="width: 100%; background: #1d9bf0; color: white; border: none; border-radius: 20px; padding: 12px 16px; cursor: pointer; font-size: 14px; font-weight: 500; margin-bottom: 12px;"> - Import All Bookmarks - </button> - - <div id="${ELEMENT_IDS.TWITTER_IMPORT_STATUS}"></div> - </div> - ` - : ` - <div style="text-align: center;"> - <p style="color: #536471; font-size: 14px; margin: 0 0 12px 0;"> - Please sign in to supermemory first - </p> - <button id="${ELEMENT_IDS.TWITTER_SIGNIN_BTN}" style="background: #1d9bf0; color: white; border: none; border-radius: 20px; padding: 8px 16px; cursor: pointer; font-size: 14px; font-weight: 500;"> - Sign In - </button> - </div> - ` - } - - <style> - @keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } - } - </style> - ` - - // Add event listeners - const closeBtn = container.querySelector(`#${ELEMENT_IDS.TWITTER_CLOSE_BTN}`) - closeBtn?.addEventListener("click", onClose) - - const importBtn = container.querySelector( - `#${ELEMENT_IDS.TWITTER_IMPORT_BTN}`, - ) - importBtn?.addEventListener("click", onImport) - - const signinBtn = container.querySelector( - `#${ELEMENT_IDS.TWITTER_SIGNIN_BTN}`, - ) - signinBtn?.addEventListener("click", () => { - browser.tabs.create({ url: `${API_ENDPOINTS.SUPERMEMORY_WEB}/login` }) - }) - - return container -} - -/** * Creates a save tweet element button for Twitter/X * @param onClick - Click handler for the button * @returns HTMLElement - The save button element @@ -510,7 +414,48 @@ export const DOMUtils = { state: ToastState, duration: number = UI_CONFIG.TOAST_DURATION, ): HTMLElement { - // Remove all existing toasts more aggressively + const existingToast = document.getElementById(ELEMENT_IDS.SUPERMEMORY_TOAST) + + if ((state === "success" || state === "error") && existingToast) { + const icon = existingToast.querySelector("div") + const text = existingToast.querySelector("span") + + if (icon && text) { + // Update based on new state + if (state === "success") { + const iconUrl = browser.runtime.getURL("/icon-16.png") + icon.innerHTML = `<img src="${iconUrl}" width="20" height="20" alt="Success" style="border-radius: 2px;" />` + icon.style.animation = "" + text.textContent = "Added to Memory" + } else if (state === "error") { + icon.innerHTML = ` + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> + <circle cx="12" cy="12" r="10" fill="#ef4444"/> + <path d="M15 9L9 15" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> + <path d="M9 9L15 15" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> + </svg> + ` + icon.style.animation = "" + text.textContent = + "Failed to save memory / Make sure you are logged in" + } + + // Auto-dismiss + setTimeout(() => { + if (document.body.contains(existingToast)) { + existingToast.style.animation = "fadeOut 0.3s ease-out" + setTimeout(() => { + if (document.body.contains(existingToast)) { + existingToast.remove() + } + }, 300) + } + }, duration) + + return existingToast + } + } + const existingToasts = document.querySelectorAll( `#${ELEMENT_IDS.SUPERMEMORY_TOAST}`, ) |