diff --git a/README.md b/README.md index 82f5b10b..fd5ba4ef 100644 --- a/README.md +++ b/README.md @@ -213,6 +213,19 @@ The following shows several events that allow for detailed logging and inspectio > The logging can be useful for analysis and instrumentation of production apps > where no console access is possible +#### Runtime verbose logging + +The `logger`, `isVerbose`, and `isPrivate` options passed to `Meteor.connect` +can also be updated after connecting: + +```js +Meteor.setLoggingOptions({ + isVerbose: true, + isPrivate: false, + logger: (message) => console.debug(message), +}); +``` + #### Data level events (high level) The most convenient way to track internals is via `Data.onChange`: diff --git a/lib/ddp.ts b/lib/ddp.ts index 46964ed5..0d4abb90 100644 --- a/lib/ddp.ts +++ b/lib/ddp.ts @@ -86,6 +86,8 @@ interface DDPOptions { deferReplayUntilLogin?: boolean; } +type DDPLoggingOptions = Pick; + /** * The default timout in ms until a reconnection attempt starts * @type {number} @@ -260,21 +262,20 @@ class DDP extends EventEmitter { this.socket = new Socket(options.SocketConstructor, options.endpoint); - if (this.isVerbose) { - this.socket.on('message:out', (outMessage) => { - try { - const { params, ...rest } = outMessage as any; - const base = { SEND: 'SEND', ...rest }; - this.logger( - this.isPrivate && params !== undefined - ? base - : { SEND: 'SEND', ...outMessage } - ); - } catch (e) { - // no-op - } - }); - } + this.socket.on('message:out', (outMessage) => { + if (!this.isVerbose) return; + try { + const { params, ...rest } = outMessage as any; + const base = { SEND: 'SEND', ...rest }; + this.logger( + this.isPrivate && params !== undefined + ? base + : { SEND: 'SEND', ...outMessage } + ); + } catch (e) { + // no-op + } + }); this.socket.on('open', () => { this.isVerbose && @@ -455,6 +456,23 @@ class DDP extends EventEmitter { this.socket.close(); } + setLoggingOptions(options: DDPLoggingOptions): void { + if (options.logger !== undefined) { + this.logger = options.logger; + } + if (options.isPrivate !== undefined) { + this.isPrivate = options.isPrivate; + } + if (options.isVerbose !== undefined) { + this.isVerbose = options.isVerbose; + } + + this.messageQueue.setLoggingOptions({ + logger: this.logger, + isVerbose: this.isVerbose, + }); + } + /** * Pushes a method to the message queue. * This is what happens under the hood when using {Meteor.call} diff --git a/lib/queue.ts b/lib/queue.ts index 0b3b1c42..1efd81da 100644 --- a/lib/queue.ts +++ b/lib/queue.ts @@ -36,6 +36,15 @@ export default class Queue { this.isVerbose = options.isVerbose ?? false; } + setLoggingOptions(options: QueueOptions): void { + if (options.logger !== undefined) { + this.logger = options.logger; + } + if (options.isVerbose !== undefined) { + this.isVerbose = options.isVerbose; + } + } + /** * Adds a new element to the queue * @param element {any} likely an object diff --git a/package-lock.json b/package-lock.json index ea80accb..3af69fa4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@meteorrn/core", - "version": "2.28.2", + "version": "2.29.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@meteorrn/core", - "version": "2.28.2", + "version": "2.29.0", "license": "MIT", "dependencies": { "@meteorrn/minimongo": "1.0.1", diff --git a/package.json b/package.json index da9a6ee8..5466eaed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@meteorrn/core", - "version": "2.28.2", + "version": "2.29.0", "description": "Meteor Client for React Native", "type": "module", "main": "dist/src/index.js", diff --git a/src/Meteor.ts b/src/Meteor.ts index 4f0d8c83..e680c62f 100644 --- a/src/Meteor.ts +++ b/src/Meteor.ts @@ -3,7 +3,7 @@ import EJSON from 'ejson'; import DDP from '../lib/ddp'; import Random from '../lib/Random'; -import Data, { type LoggerPayload } from './Data'; +import Data, { type ConnectOptions, type LoggerPayload } from './Data'; import Mongo from './Mongo'; import { Collection, getObservers, localCollections } from './Collection'; import call from './Call'; @@ -20,6 +20,11 @@ type DdpErrorPayload = { details?: any; }; +export type MeteorLoggingOptions = Pick< + ConnectOptions, + 'isVerbose' | 'isPrivate' | 'logger' +>; + function toMeteorStyleError( payload?: DdpErrorPayload | null ): Error | undefined { @@ -55,6 +60,7 @@ export interface MeteorBase { waitDdpConnected: (cb: (...args: any[]) => void) => void; reconnect(): void; connect(endpoint?: string, options?: any): void; + setLoggingOptions(options: MeteorLoggingOptions): void; requireDdp(): DDP; subscribe( name: string, @@ -166,6 +172,22 @@ const Meteor: MeteorBase = { reconnect() { Data.ddp && Data.ddp.connect(); }, + setLoggingOptions(options: MeteorLoggingOptions) { + if (options.isVerbose !== undefined) { + this.isVerbose = options.isVerbose; + } + if (options.logger !== undefined) { + this.logger = options.logger; + } + + Data._options = { + ...Data._options, + ...Object.fromEntries( + Object.entries(options).filter(([, value]) => value !== undefined) + ), + }; + Data.ddp?.setLoggingOptions(options); + }, /** * Connect to a Meteor server using a given websocket endpoint. * The endpoint needs to start with `ws://` or `wss://` diff --git a/src/index.ts b/src/index.ts index b27e06d1..56901bad 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,5 +12,6 @@ const { useTracker, withTracker, Mongo, ReactiveDict } = MeteorAugmented; export { useTracker, Accounts, withTracker, Mongo, ReactiveDict, Tracker }; export { Vent } from './vent'; +export type { MeteorLoggingOptions } from './Meteor'; export type { LoginFailurePayload } from './user/User'; export default MeteorAugmented; diff --git a/test/lib/ddp.tests.js b/test/lib/ddp.tests.js index 7249f705..e02b0bd8 100644 --- a/test/lib/ddp.tests.js +++ b/test/lib/ddp.tests.js @@ -292,4 +292,47 @@ describe('ddp', function () { }); }); }); + describe('logging', function () { + it('updates logging options at runtime', function (done) { + const logs = []; + validOptions.autoConnect = false; + validOptions.autoReconnect = false; + validOptions.isVerbose = false; + validOptions.isPrivate = true; + validOptions.logger = (message) => logs.push(message); + + ddp = new DDP(validOptions); + ddp.setLoggingOptions({ + isVerbose: true, + isPrivate: false, + }); + + listen(ddp.socket, 'open', () => { + try { + ddp.socket.emit('message:in', { + msg: 'connected', + session: 'session', + }); + ddp.method('foo', { foo: 'bar' }); + + const sentMethod = logs.find( + (message) => message.SEND === 'SEND' && message.msg === 'method' + ); + const queuedMethod = logs.find( + (message) => message.QUEUE === 'ENQUEUE' && message.msg === 'method' + ); + + expect(sentMethod).to.exist; + expect(queuedMethod).to.exist; + expect(sentMethod.params).to.deep.equal({ foo: 'bar' }); + expect(queuedMethod.params).to.equal(undefined); + done(); + } catch (error) { + done(error); + } + }); + + ddp.connect(); + }); + }); }); diff --git a/test/src/Meteor.tests.js b/test/src/Meteor.tests.js index 3f3837c2..e67b9023 100644 --- a/test/src/Meteor.tests.js +++ b/test/src/Meteor.tests.js @@ -92,4 +92,46 @@ describe('Meteor - integration', function () { }); }); }); + + describe(Meteor.setLoggingOptions.name, () => { + it('updates Meteor and current DDP logging options', () => { + const data = Meteor.getData(); + const previousDdp = data.ddp; + const previousOptions = data._options; + const previousIsVerbose = Meteor.isVerbose; + const previousLogger = Meteor.logger; + const logger = () => {}; + const calls = []; + + data.ddp = { + setLoggingOptions: (options) => calls.push(options), + }; + + try { + Meteor.setLoggingOptions({ + isVerbose: true, + isPrivate: false, + logger, + }); + + expect(Meteor.isVerbose).to.equal(true); + expect(Meteor.logger).to.equal(logger); + expect(data._options.isVerbose).to.equal(true); + expect(data._options.isPrivate).to.equal(false); + expect(data._options.logger).to.equal(logger); + expect(calls).to.deep.equal([ + { + isVerbose: true, + isPrivate: false, + logger, + }, + ]); + } finally { + data.ddp = previousDdp; + data._options = previousOptions; + Meteor.isVerbose = previousIsVerbose; + Meteor.logger = previousLogger; + } + }); + }); });