diff --git a/.changeset/quiet-owls-flash.md b/.changeset/quiet-owls-flash.md new file mode 100644 index 000000000..51c3f8511 --- /dev/null +++ b/.changeset/quiet-owls-flash.md @@ -0,0 +1,18 @@ +--- +'@powersync/react-native': minor +'@powersync/common': minor +'@powersync/node': minor +'@powersync/web': minor +'@powersync/capacitor': minor +--- + +Added ability to specify `appMetadata` for sync/stream requests + +```javascript +powerSync.connect(connector, { + // This will be included in PowerSync service logs + appMetadata: { + app_version: MY_APP_VERSION + } +}); +``` diff --git a/demos/example-node/src/main.ts b/demos/example-node/src/main.ts index f64361f0a..bfef05d51 100644 --- a/demos/example-node/src/main.ts +++ b/demos/example-node/src/main.ts @@ -10,10 +10,9 @@ import { SyncStreamConnectionMethod } from '@powersync/node'; import { exit } from 'node:process'; +import { WorkerOpener } from 'node_modules/@powersync/node/src/db/options.js'; import { AppSchema, DemoConnector } from './powersync.js'; import { enableUncidiDiagnostics } from './UndiciDiagnostics.js'; -import { WorkerOpener } from 'node_modules/@powersync/node/src/db/options.js'; -import { LockContext } from 'node_modules/@powersync/node/dist/bundle.cjs'; const main = async () => { const baseLogger = createBaseLogger(); @@ -59,10 +58,12 @@ const main = async () => { logger }); console.log(await db.get('SELECT powersync_rs_version();')); - await db.connect(new DemoConnector(), { connectionMethod: SyncStreamConnectionMethod.WEB_SOCKET, - clientImplementation: SyncClientImplementation.RUST + clientImplementation: SyncClientImplementation.RUST, + appMetadata: { + app_version: process.env.npm_package_version || 'unknown' + } }); // Example using a proxy agent for more control over the connection: // const proxyAgent = new (await import('undici')).ProxyAgent({ diff --git a/demos/react-supabase-todolist/src/components/providers/SystemProvider.tsx b/demos/react-supabase-todolist/src/components/providers/SystemProvider.tsx index 8a3f3c209..6bcf627de 100644 --- a/demos/react-supabase-todolist/src/components/providers/SystemProvider.tsx +++ b/demos/react-supabase-todolist/src/components/providers/SystemProvider.tsx @@ -7,6 +7,8 @@ import { createBaseLogger, DifferentialWatchedQuery, LogLevel, PowerSyncDatabase import React, { Suspense } from 'react'; import { NavigationPanelContextProvider } from '../navigation/NavigationPanelContext'; +declare const APP_VERSION: string; + const SupabaseContext = React.createContext(null); export const useSupabase = () => React.useContext(SupabaseContext); @@ -68,7 +70,11 @@ export const SystemProvider = ({ children }: { children: React.ReactNode }) => { const l = connector.registerListener({ initialized: () => {}, sessionStarted: () => { - powerSync.connect(connector); + powerSync.connect(connector, { + appMetadata: { + app_version: APP_VERSION + } + }); } }); diff --git a/demos/react-supabase-todolist/vite.config.mts b/demos/react-supabase-todolist/vite.config.mts index ab0b7d202..ea84c4153 100644 --- a/demos/react-supabase-todolist/vite.config.mts +++ b/demos/react-supabase-todolist/vite.config.mts @@ -1,9 +1,9 @@ -import wasm from 'vite-plugin-wasm'; -import topLevelAwait from 'vite-plugin-top-level-await'; import { fileURLToPath, URL } from 'url'; +import topLevelAwait from 'vite-plugin-top-level-await'; +import wasm from 'vite-plugin-wasm'; -import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; import { VitePWA } from 'vite-plugin-pwa'; // https://vitejs.dev/config/ @@ -19,6 +19,9 @@ export default defineConfig({ resolve: { alias: [{ find: '@', replacement: fileURLToPath(new URL('./src', import.meta.url)) }] }, + define: { + APP_VERSION: JSON.stringify(process.env.npm_package_version) + }, publicDir: '../public', envDir: '..', // Use this dir for env vars, not 'src'. optimizeDeps: { diff --git a/packages/common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts b/packages/common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts index ab3e37c7c..6a2b73da7 100644 --- a/packages/common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +++ b/packages/common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts @@ -16,7 +16,7 @@ import { import { CrudEntry } from '../bucket/CrudEntry.js'; import { SyncDataBucket } from '../bucket/SyncDataBucket.js'; import { AbstractRemote, FetchStrategy, SyncStreamOptions } from './AbstractRemote.js'; -import { coreStatusToJs, EstablishSyncStream, Instruction, SyncPriorityStatus } from './core-instruction.js'; +import { EstablishSyncStream, Instruction, coreStatusToJs } from './core-instruction.js'; import { BucketRequest, CrudUploadNotification, @@ -129,6 +129,11 @@ export interface InternalConnectionOptions extends BaseConnectionOptions, Additi /** @internal */ export interface BaseConnectionOptions { + /** + * A set of metadata to be included in service logs. + */ + appMetadata?: Record; + /** * Whether to use a JavaScript implementation to handle received sync lines from the sync * service, or whether this work should be offloaded to the PowerSync core extension. @@ -223,6 +228,7 @@ export const DEFAULT_STREAMING_SYNC_OPTIONS = { export type RequiredPowerSyncConnectionOptions = Required; export const DEFAULT_STREAM_CONNECTION_OPTIONS: RequiredPowerSyncConnectionOptions = { + appMetadata: {}, connectionMethod: SyncStreamConnectionMethod.WEB_SOCKET, clientImplementation: DEFAULT_SYNC_CLIENT_IMPLEMENTATION, fetchStrategy: FetchStrategy.Buffered, @@ -658,6 +664,16 @@ The next upload iteration will be delayed.`); ...DEFAULT_STREAM_CONNECTION_OPTIONS, ...(options ?? {}) }; + + // Validate app metadata + const invalidMetadata = Object.entries(resolvedOptions.appMetadata).filter( + ([_, value]) => typeof value != 'string' + ); + if (invalidMetadata.length > 0) { + throw new Error( + `Invalid appMetadata provided. Only string values are allowed. Invalid values: ${invalidMetadata.map(([key, value]) => `${key}: ${value}`).join(', ')}` + ); + } const clientImplementation = resolvedOptions.clientImplementation; this.updateSyncStatus({ clientImplementation }); @@ -699,6 +715,7 @@ The next upload iteration will be delayed.`); include_checksum: true, raw_data: true, parameters: resolvedOptions.params, + app_metadata: resolvedOptions.appMetadata, client_id: clientId } }; @@ -947,7 +964,14 @@ The next upload iteration will be delayed.`); const syncOptions: SyncStreamOptions = { path: '/sync/stream', abortSignal: abortController.signal, - data: instr.request + data: { + ...instr.request, + // FIXME the Rust core does not currently pass metadata through + app_metadata: { + ...resolvedOptions.appMetadata, + ...instr.request.app_metadata + } + } }; if (resolvedOptions.connectionMethod == SyncStreamConnectionMethod.HTTP) { @@ -1088,6 +1112,7 @@ The next upload iteration will be delayed.`); try { const options: any = { parameters: resolvedOptions.params, + app_metadata: resolvedOptions.appMetadata, active_streams: this.activeStreams, include_defaults: resolvedOptions.includeDefaultStreams }; diff --git a/packages/common/src/client/sync/stream/streaming-sync-types.ts b/packages/common/src/client/sync/stream/streaming-sync-types.ts index fdb73f360..8fa10520c 100644 --- a/packages/common/src/client/sync/stream/streaming-sync-types.ts +++ b/packages/common/src/client/sync/stream/streaming-sync-types.ts @@ -90,6 +90,11 @@ export interface StreamingSyncRequest { */ parameters?: Record; + /** + * Application metadata to be included in service logs. + */ + app_metadata?: Record; + client_id?: string; }