|
| 1 | +[npm-image]: https://img.shields.io/npm/v/@huolala-tech/page-spy-lynx?logo=npm&label=version |
| 2 | +[npm-url]: https://www.npmjs.com/package/@huolala-tech/page-spy-lynx |
| 3 | +[minified-image]: https://img.shields.io/bundlephobia/min/@huolala-tech/page-spy-lynx |
| 4 | +[minified-url]: https://unpkg.com/browse/@huolala-tech/page-spy-lynx/dist/esm/index.min.js |
| 5 | + |
| 6 | +[English](./README.md) | [Chinese](./README_ZH.md) |
| 7 | + |
| 8 | +# `@huolala-tech/page-spy-lynx` |
| 9 | + |
| 10 | +[![SDK version][npm-image]][npm-url] |
| 11 | +[![SDK size][minified-image]][minified-url] |
| 12 | + |
| 13 | +> PageSpy SDK for Lynx apps. |
| 14 | +
|
| 15 | +Lynx does not provide browser `window` or `document` objects. This SDK does not depend on DOM APIs, and its built-in plugins enable themselves only when the Lynx runtime exposes the corresponding global capability. |
| 16 | + |
| 17 | +## Usage |
| 18 | + |
| 19 | +Initialize PageSpy from background-only code, such as an entry module imported only by background logic or inside a Lynx `useEffect`. |
| 20 | + |
| 21 | +```ts |
| 22 | +import PageSpy from '@huolala-tech/page-spy-lynx'; |
| 23 | + |
| 24 | +const pageSpy = new PageSpy({ |
| 25 | + api: 'example.com', |
| 26 | +}); |
| 27 | +``` |
| 28 | + |
| 29 | +## `InitConfig` |
| 30 | + |
| 31 | +The `api` option is required. Other options follow the shared PageSpy SDK config. |
| 32 | + |
| 33 | +```ts |
| 34 | +const pageSpy = new PageSpy(config); |
| 35 | + |
| 36 | +interface InitConfig { |
| 37 | + api: string; |
| 38 | + project?: string; |
| 39 | + title?: string; |
| 40 | + enableSSL?: boolean | null; |
| 41 | + disabledPlugins?: (InternalPlugins | string)[]; |
| 42 | +} |
| 43 | + |
| 44 | +type InternalPlugins = |
| 45 | + | 'ConsolePlugin' |
| 46 | + | 'ErrorPlugin' |
| 47 | + | 'NetworkPlugin' |
| 48 | + | 'SystemPlugin' |
| 49 | + | 'WebSocketPlugin'; |
| 50 | +``` |
| 51 | + |
| 52 | +## Built-in plugins |
| 53 | + |
| 54 | +- `ConsolePlugin`: proxies `console.log/info/error/warn/debug`. |
| 55 | +- `SystemPlugin`: reports PageSpy client and available Lynx system information. |
| 56 | +- `NetworkPlugin`: proxies `fetch` first, and proxies `XMLHttpRequest` only when the runtime exposes a compatible implementation. |
| 57 | +- `WebSocketPlugin`: proxies WebSocket traffic only when `globalThis.WebSocket` is constructable. |
| 58 | +- `ErrorPlugin`: listens to global error and unhandled rejection hooks when the runtime exposes them. |
| 59 | + |
| 60 | +Use `disabledPlugins` to turn off any built-in plugin. |
| 61 | + |
| 62 | +## Native WebSocket module |
| 63 | + |
| 64 | +PageSpy prefers Lynx `NativeModules.LynxNativeWebSocketModule` for its own WebSocket connection. Host apps should register this native module before initializing PageSpy in real Lynx native runtimes. In web preview runtimes, the SDK falls back to a constructable `globalThis.WebSocket`; if neither capability exists, initialization throws a clear error. |
| 65 | + |
| 66 | +The JavaScript contract is: |
| 67 | + |
| 68 | +```ts |
| 69 | +declare let NativeModules: { |
| 70 | + LynxNativeWebSocketModule: { |
| 71 | + connect(socketId: string, url: string): void; |
| 72 | + send(socketId: string, data: string): void; |
| 73 | + close(socketId: string): void; |
| 74 | + drainEvents( |
| 75 | + socketId: string, |
| 76 | + callback: (events: NativeWebSocketEvent[]) => void, |
| 77 | + ): void; |
| 78 | + }; |
| 79 | +}; |
| 80 | + |
| 81 | +type NativeWebSocketEvent = |
| 82 | + | { type: 'open'; socketId: string } |
| 83 | + | { type: 'message'; socketId: string; data: string } |
| 84 | + | { type: 'close'; socketId: string; code?: number; reason?: string } |
| 85 | + | { type: 'error'; socketId: string; message?: string }; |
| 86 | +``` |
| 87 | + |
| 88 | +Native implementations should enqueue `open/message/close/error` events and return them through `drainEvents`. Do not depend on invoking the same NativeModule callback multiple times. |
| 89 | + |
| 90 | +Native examples are included in `native-examples/android`, `native-examples/ios`, and `native-examples/harmony`. |
| 91 | + |
| 92 | +Android registration: |
| 93 | + |
| 94 | +```java |
| 95 | +LynxEnv.inst().registerModule( |
| 96 | + "LynxNativeWebSocketModule", |
| 97 | + LynxNativeWebSocketModule.class |
| 98 | +); |
| 99 | +``` |
| 100 | + |
| 101 | +The Android example uses OkHttp's `WebSocketListener`; add OkHttp to the host app or adapt the sample to the app's existing WebSocket client. |
| 102 | + |
| 103 | +iOS registration: |
| 104 | + |
| 105 | +```objc |
| 106 | +[globalConfig registerModule:LynxNativeWebSocketModule.class]; |
| 107 | +``` |
| 108 | +
|
| 109 | +Harmony registration: |
| 110 | +
|
| 111 | +```ts |
| 112 | +import { LynxNativeWebSocketModule } from './module/LynxNativeWebSocketModule'; |
| 113 | +
|
| 114 | +this.modules.set('LynxNativeWebSocketModule', { |
| 115 | + moduleClass: LynxNativeWebSocketModule, |
| 116 | +}); |
| 117 | +``` |
| 118 | + |
| 119 | +The Harmony example uses NetworkKit's WebSocket API. If your Harmony SDK uses a different WebSocket import path, adapt only the import and keep the NativeModule contract unchanged. |
| 120 | + |
| 121 | +`WebSocketPlugin` is separate from this module. It still proxies app WebSocket traffic only when the Lynx runtime exposes a constructable `globalThis.WebSocket`. |
| 122 | + |
| 123 | +## Native Console module |
| 124 | + |
| 125 | +In Lynx native runtimes, `console` messages are routed through the Lynx DevTool inspector pipeline and cannot be reliably intercepted by overwriting `console.log` in JavaScript. PageSpy automatically detects `NativeModules.PageSpyConsoleModule` and polls `drainMessages` for JSON strings received by `LynxInspectorConsoleDelegate.onConsoleMessage(msg)`. |
| 126 | + |
| 127 | +The JavaScript contract is: |
| 128 | + |
| 129 | +```ts |
| 130 | +declare let NativeModules: { |
| 131 | + PageSpyConsoleModule: { |
| 132 | + drainMessages( |
| 133 | + callback: (messages: string[]) => void, |
| 134 | + ): void; |
| 135 | + }; |
| 136 | +}; |
| 137 | +``` |
| 138 | + |
| 139 | +Native examples are included in `native-examples/android`, `native-examples/ios`, and `native-examples/harmony`: |
| 140 | + |
| 141 | +- `PageSpyConsoleModule`: stores console messages and returns them to JS via `drainMessages`. |
| 142 | +- `PageSpyConsoleDelegate`: implements `LynxInspectorConsoleDelegate` and queues messages from `onConsoleMessage(msg)`. |
| 143 | + |
| 144 | +Android registration and setup: |
| 145 | + |
| 146 | +```java |
| 147 | +LynxEnv.inst().registerModule( |
| 148 | + "PageSpyConsoleModule", |
| 149 | + PageSpyConsoleModule.class |
| 150 | +); |
| 151 | + |
| 152 | +LynxBaseInspectorOwner owner = lynxView.getBaseInspectorOwner(); |
| 153 | +if (owner != null) { |
| 154 | + owner.setLynxInspectorConsoleDelegate(new PageSpyConsoleDelegate()); |
| 155 | +} |
| 156 | +``` |
| 157 | + |
| 158 | +iOS registration and setup: |
| 159 | + |
| 160 | +```objc |
| 161 | +[globalConfig registerModule:PageSpyConsoleModule.class]; |
| 162 | + |
| 163 | +id<LynxBaseInspectorOwner> owner = lynxView.baseInspectorOwner; |
| 164 | +if (owner) { |
| 165 | + [PageSpyConsoleDelegate installWithInspectorOwner:owner]; |
| 166 | +} |
| 167 | +``` |
| 168 | +
|
| 169 | +Harmony registration and setup: |
| 170 | +
|
| 171 | +```ts |
| 172 | +import { LynxView, LynxContext } from '@lynx/lynx'; |
| 173 | +import { PageSpyConsoleModule } from './module/PageSpyConsoleModule'; |
| 174 | +import { PageSpyConsoleDelegate } from './module/PageSpyConsoleDelegate'; |
| 175 | +
|
| 176 | +this.modules.set('PageSpyConsoleModule', { |
| 177 | + moduleClass: PageSpyConsoleModule, |
| 178 | +}); |
| 179 | +
|
| 180 | +LynxView({ |
| 181 | + onCreate: (context: LynxContext) => { |
| 182 | + const owner = context.getBaseInspectorOwner(); |
| 183 | + if (owner) { |
| 184 | + PageSpyConsoleDelegate.installWithInspectorOwner(owner); |
| 185 | + } |
| 186 | + }, |
| 187 | +}); |
| 188 | +``` |
| 189 | + |
| 190 | +Without this delegate integration, PageSpy still works in Web or JS-overwritable console environments, but iOS/Android/Harmony native console messages may not be captured because they stay inside Lynx's native inspector pipeline. |
| 191 | + |
| 192 | +Troubleshooting: if iOS logs only show `LynxNativeWebSocketModule.drainEvents` and never show `PageSpyConsoleModule.drainMessages`, the current LynxView does not expose `NativeModules.PageSpyConsoleModule`. Registering only the WebSocket module makes the PageSpy connection work, but it does not capture native console messages. |
0 commit comments