@appxiom/4react 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +5 -0
- package/README.md +252 -0
- package/core/ax-react.js +18 -0
- package/index.d.ts +19 -0
- package/index.js +9 -0
- package/lifecycle/patch.js +296 -0
- package/memory/ax-react-useref-leak.js +15 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Copyright (c) 2026 BasilGregory Software Labs Private Limited
|
|
2
|
+
|
|
3
|
+
All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software is licensed, not sold. No part of this software may be copied, modified, distributed, reverse engineered, or used except as expressly permitted by BasilGregory Software Labs Private Limited.
|
package/README.md
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# Appxiom React SDK (`@appxiom/4react`)
|
|
2
|
+
|
|
3
|
+
`@appxiom/4react` initializes the Appxiom browser SDK and adds React-aware instrumentation for application lifecycle activity and `useRef` leak detection. It depends on `@appxiom/core`, which npm installs automatically.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Requirements](#requirements)
|
|
8
|
+
- [Installation](#installation)
|
|
9
|
+
- [Quick Start](#quick-start)
|
|
10
|
+
- [TypeScript](#typescript)
|
|
11
|
+
- [Automatic React Instrumentation](#automatic-react-instrumentation)
|
|
12
|
+
- [Vite](#vite)
|
|
13
|
+
- [SSR Frameworks](#ssr-frameworks)
|
|
14
|
+
- [Production Deployment](#production-deployment)
|
|
15
|
+
- [Public API](#public-api)
|
|
16
|
+
- [Troubleshooting](#troubleshooting)
|
|
17
|
+
|
|
18
|
+
## Requirements
|
|
19
|
+
|
|
20
|
+
- React 18 or later.
|
|
21
|
+
- A browser environment. The SDK uses browser APIs and must not run during server-side rendering.
|
|
22
|
+
- Node.js 18 or later for package installation and builds.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
Install the React integration and React. `@appxiom/core` is installed automatically as a dependency.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install @appxiom/4react react
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
Initialize the SDK once from browser-only application startup code. Retain the returned `ax` instance to call its public API elsewhere in your application.
|
|
35
|
+
|
|
36
|
+
### JavaScript
|
|
37
|
+
|
|
38
|
+
```javascript
|
|
39
|
+
import init from '@appxiom/4react';
|
|
40
|
+
|
|
41
|
+
const ax = await init(
|
|
42
|
+
'YOUR_APP_KEY',
|
|
43
|
+
'YOUR_PLATFORM_KEY',
|
|
44
|
+
'1.0.0',
|
|
45
|
+
'build-001',
|
|
46
|
+
);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### TypeScript
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import init, { type AXReact } from '@appxiom/4react';
|
|
53
|
+
|
|
54
|
+
const ax: AXReact = await init(
|
|
55
|
+
'YOUR_APP_KEY',
|
|
56
|
+
'YOUR_PLATFORM_KEY',
|
|
57
|
+
'1.0.0',
|
|
58
|
+
'build-001',
|
|
59
|
+
);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## TypeScript
|
|
63
|
+
|
|
64
|
+
`@appxiom/4react` ships first-party TypeScript declarations. No separate `@types` package is required. `init(...)` returns `Promise<AXReact>`, and `AXReact` extends the public `AX` API from `@appxiom/core`.
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import init, { type AXReact } from '@appxiom/4react';
|
|
68
|
+
import type { IssueSeverity } from '@appxiom/core';
|
|
69
|
+
|
|
70
|
+
const ax: AXReact = await init(
|
|
71
|
+
'YOUR_APP_KEY',
|
|
72
|
+
'YOUR_PLATFORM_KEY',
|
|
73
|
+
'1.0.0',
|
|
74
|
+
'build-001',
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const severity: IssueSeverity = 1;
|
|
78
|
+
|
|
79
|
+
ax.setCustomId('user-12345');
|
|
80
|
+
ax.startGoal('checkout');
|
|
81
|
+
ax.logActivity('Checkout started', { cartValue: 120 });
|
|
82
|
+
ax.reportCustomIssue('Payment failed', 'Gateway timed out.', severity, {
|
|
83
|
+
orderId: 'ORD-10023',
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`IssueSeverity` is limited to `0` (Minor), `1` (Major), or `2` (Fatal).
|
|
88
|
+
|
|
89
|
+
## Automatic React Instrumentation
|
|
90
|
+
|
|
91
|
+
After initialization, the integration adds automatic browser-side observation for React hook activity. It records lifecycle activity associated with `useEffect`, `useLayoutEffect`, `useState`, `useReducer`, `useMemo`, and `useCallback`. It also tracks `useRef` instances for leak analysis.
|
|
92
|
+
|
|
93
|
+
Initialize the package before rendering the application so the instrumentation is available when components begin rendering. These integrations are installed once per page and do not require component-level wrappers or hooks.
|
|
94
|
+
|
|
95
|
+
## Vite
|
|
96
|
+
|
|
97
|
+
`@appxiom/4react` depends on `@appxiom/core`, which includes a WebAssembly binary. During local development, exclude both packages from Vite dependency optimization so Vite preserves the binary's source-relative URL:
|
|
98
|
+
|
|
99
|
+
### JavaScript
|
|
100
|
+
|
|
101
|
+
```javascript
|
|
102
|
+
import { defineConfig } from 'vite';
|
|
103
|
+
import react from '@vitejs/plugin-react';
|
|
104
|
+
|
|
105
|
+
export default defineConfig({
|
|
106
|
+
plugins: [react()],
|
|
107
|
+
optimizeDeps: {
|
|
108
|
+
exclude: ['@appxiom/4react', '@appxiom/core'],
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### TypeScript
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { defineConfig } from 'vite';
|
|
117
|
+
import react from '@vitejs/plugin-react';
|
|
118
|
+
|
|
119
|
+
export default defineConfig({
|
|
120
|
+
plugins: [react()],
|
|
121
|
+
optimizeDeps: {
|
|
122
|
+
exclude: ['@appxiom/4react', '@appxiom/core'],
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
After adding the configuration, clear Vite's optimized-dependency cache and restart the development server:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
rm -rf node_modules/.vite
|
|
131
|
+
npm run dev -- --force
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
This setting only affects Vite's development dependency optimizer. It is safe to keep in the configuration for production builds.
|
|
135
|
+
|
|
136
|
+
## SSR Frameworks
|
|
137
|
+
|
|
138
|
+
For Next.js, Nuxt, SvelteKit, Remix, or any other server-rendered application, load and initialize the SDK only on the client. Do not import or initialize it from server components, route handlers, or server rendering code.
|
|
139
|
+
|
|
140
|
+
### JavaScript
|
|
141
|
+
|
|
142
|
+
```javascript
|
|
143
|
+
if (typeof window !== 'undefined') {
|
|
144
|
+
const { default: init } = await import('@appxiom/4react');
|
|
145
|
+
|
|
146
|
+
const ax = await init(
|
|
147
|
+
'YOUR_APP_KEY',
|
|
148
|
+
'YOUR_PLATFORM_KEY',
|
|
149
|
+
'1.0.0',
|
|
150
|
+
'build-001',
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### TypeScript
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
if (typeof window !== 'undefined') {
|
|
159
|
+
const { default: init } = await import('@appxiom/4react');
|
|
160
|
+
|
|
161
|
+
const ax = await init(
|
|
162
|
+
'YOUR_APP_KEY',
|
|
163
|
+
'YOUR_PLATFORM_KEY',
|
|
164
|
+
'1.0.0',
|
|
165
|
+
'build-001',
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
For React client applications, initialize in the browser entry point before `createRoot(...).render(...)`. For Next.js, use a Client Component or another client-only initialization path.
|
|
171
|
+
|
|
172
|
+
## Production Deployment
|
|
173
|
+
|
|
174
|
+
Build and deploy the application normally. For a Vite application:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
npm run build
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The production build emits Appxiom's WebAssembly binary as an asset and rewrites its loader URL. Deploy the complete build output, including the generated `.wasm` file, without renaming or omitting assets.
|
|
181
|
+
|
|
182
|
+
Your CDN or web server must return this header for WebAssembly assets:
|
|
183
|
+
|
|
184
|
+
```http
|
|
185
|
+
Content-Type: application/wasm
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Ensure SPA fallback rules do not return `index.html` for `.wasm` requests. Returning HTML instead of the binary causes errors such as `expected magic word 00 61 73 6d`.
|
|
189
|
+
|
|
190
|
+
## Public API
|
|
191
|
+
|
|
192
|
+
### `init(appKey, platformKey, version, buildIdentifier)`
|
|
193
|
+
|
|
194
|
+
Initializes the core SDK, installs the React instrumentation, and returns `Promise<AXReact>`.
|
|
195
|
+
|
|
196
|
+
| Parameter | Type | Description |
|
|
197
|
+
| --- | --- | --- |
|
|
198
|
+
| `appKey` | `string` | Appxiom application key. |
|
|
199
|
+
| `platformKey` | `string` | Appxiom platform key. |
|
|
200
|
+
| `version` | `string` | Application version. |
|
|
201
|
+
| `buildIdentifier` | `string` | Optional build or release identifier. |
|
|
202
|
+
|
|
203
|
+
### Inherited `AX` methods
|
|
204
|
+
|
|
205
|
+
The returned `AXReact` instance inherits these supported methods from `@appxiom/core`:
|
|
206
|
+
|
|
207
|
+
- `setCustomId(customId)`: associates telemetry with an application user or account identifier.
|
|
208
|
+
- `startGoal(name)`: starts a named goal.
|
|
209
|
+
- `endGoal(name, properties?, value?)`: ends a named goal with optional metadata and numeric value.
|
|
210
|
+
- `logActivity(name, properties?)`: records a custom activity with optional metadata.
|
|
211
|
+
- `reportCustomIssue(title, description, severity, data?, stacktrace?)`: reports an application-defined issue.
|
|
212
|
+
|
|
213
|
+
#### JavaScript
|
|
214
|
+
|
|
215
|
+
```javascript
|
|
216
|
+
ax.setCustomId('user-12345');
|
|
217
|
+
ax.startGoal('checkout');
|
|
218
|
+
ax.logActivity('Checkout started', { cartValue: 120 });
|
|
219
|
+
ax.endGoal('checkout', { orderId: 'ORD-10023' }, 120);
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
#### TypeScript
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
import type { IssueSeverity } from '@appxiom/core';
|
|
226
|
+
|
|
227
|
+
const severity: IssueSeverity = 1;
|
|
228
|
+
|
|
229
|
+
ax.setCustomId('user-12345');
|
|
230
|
+
ax.startGoal('checkout');
|
|
231
|
+
ax.logActivity('Checkout started', { cartValue: 120 });
|
|
232
|
+
ax.reportCustomIssue('Payment failed', 'Gateway timed out.', severity, {
|
|
233
|
+
orderId: 'ORD-10023',
|
|
234
|
+
});
|
|
235
|
+
ax.endGoal('checkout', { orderId: 'ORD-10023' }, 120);
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
For the complete core API and deployment details, see the `@appxiom/core` package documentation.
|
|
239
|
+
|
|
240
|
+
## Troubleshooting
|
|
241
|
+
|
|
242
|
+
### Vite development reports a WASM MIME type or `expected magic word` error
|
|
243
|
+
|
|
244
|
+
Add the Vite `optimizeDeps.exclude` configuration above, delete `node_modules/.vite`, and restart the development server with `--force`.
|
|
245
|
+
|
|
246
|
+
### Production reports `expected magic word 00 61 73 6d`
|
|
247
|
+
|
|
248
|
+
Confirm that the generated `.wasm` asset was deployed, that no SPA fallback rewrites its request to `index.html`, and that the response MIME type is `application/wasm`.
|
|
249
|
+
|
|
250
|
+
### The SDK runs during server rendering
|
|
251
|
+
|
|
252
|
+
Move the package import and `init(...)` call to browser-only code. Use a client component, framework client plugin, browser entry point, or a dynamic import guarded by `typeof window !== 'undefined'`.
|
package/core/ax-react.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { AX } from '@appxiom/core';
|
|
2
|
+
|
|
3
|
+
// Singleton AXReact class extending AX
|
|
4
|
+
export class AXReact extends AX {
|
|
5
|
+
constructor(appKey, platformKey, version, buildIdentifier) {
|
|
6
|
+
super(appKey, platformKey, version, buildIdentifier);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async launch() {
|
|
10
|
+
await super.launch();
|
|
11
|
+
// Load runtime patches synchronously using dynamic imports
|
|
12
|
+
|
|
13
|
+
await import('../memory/ax-react-useref-leak.js');
|
|
14
|
+
await import('../lifecycle/patch.js');
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
}
|
|
18
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AX } from '@appxiom/core';
|
|
2
|
+
|
|
3
|
+
export class AXReact extends AX {
|
|
4
|
+
constructor(
|
|
5
|
+
appKey: string,
|
|
6
|
+
platformKey: string,
|
|
7
|
+
version: string,
|
|
8
|
+
buildIdentifier?: string,
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
launch(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function init(
|
|
15
|
+
appKey: string,
|
|
16
|
+
platformKey: string,
|
|
17
|
+
version: string,
|
|
18
|
+
buildIdentifier?: string,
|
|
19
|
+
): Promise<AXReact>;
|
package/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { AXReact } from './core/ax-react.js';
|
|
2
|
+
|
|
3
|
+
export default async function init(appKey, platformKey, version, buildIdentifier) {
|
|
4
|
+
const ax = new AXReact(appKey, platformKey, version, buildIdentifier);
|
|
5
|
+
await ax.launch();
|
|
6
|
+
return ax;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export { AXReact };
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// React lifecycle tracker module
|
|
2
|
+
// React 18 / 19 compatible – dispatcher-based interception
|
|
3
|
+
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { AX } from '@appxiom/core';
|
|
6
|
+
import { useRefLeakTracker } from '../memory/ax-react-useref-leak.js';
|
|
7
|
+
|
|
8
|
+
function emitLifecycleEvent(name, payload = {}) {
|
|
9
|
+
try {
|
|
10
|
+
const instance = AX.instance;
|
|
11
|
+
if (!instance?.logEventActivity) return;
|
|
12
|
+
instance.logEventActivity(name, payload);
|
|
13
|
+
} catch {
|
|
14
|
+
// Never block app execution for lifecycle logging.
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function lifecycleEventName(component, action) {
|
|
19
|
+
const label = component || 'Anonymous';
|
|
20
|
+
return `${label} :: ${action}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* -------------------------------------------------------
|
|
24
|
+
* Component name (best-effort, non-fatal)
|
|
25
|
+
* ----------------------------------------------------- */
|
|
26
|
+
function toComponentLabel(value) {
|
|
27
|
+
if (!value) return 'Anonymous';
|
|
28
|
+
|
|
29
|
+
const cleaned = String(value)
|
|
30
|
+
.replace(/^Object\./, '')
|
|
31
|
+
.replace(/:\d+:\d+$/, '')
|
|
32
|
+
.replace(/\?.*$/, '')
|
|
33
|
+
.replace(/#.*$/, '');
|
|
34
|
+
|
|
35
|
+
const fileOrSymbol = cleaned.split('/').pop() || cleaned;
|
|
36
|
+
const stem = fileOrSymbol.replace(/\.(jsx|tsx|js|ts)$/i, '');
|
|
37
|
+
|
|
38
|
+
// Prefer component-ish names (capitalized), otherwise keep best effort label.
|
|
39
|
+
if (/^[A-Z][A-Za-z0-9_$]*$/.test(stem)) {
|
|
40
|
+
return stem;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return stem || 'Anonymous';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeFrame(frameLine) {
|
|
47
|
+
const symbolMatch = frameLine.match(/at\s+([^\s(]+)/);
|
|
48
|
+
if (symbolMatch?.[1]) {
|
|
49
|
+
const symbol = symbolMatch[1].replace(/^Object\./, '');
|
|
50
|
+
|
|
51
|
+
// Stack frames in Vite dev can be raw URLs like
|
|
52
|
+
// http://localhost:5173/src/App.jsx:845:39
|
|
53
|
+
// Normalize these to file/component-ish names.
|
|
54
|
+
if (symbol.includes('://') || symbol.includes('/')) {
|
|
55
|
+
const cleaned = symbol
|
|
56
|
+
.replace(/^https?:\/\//, '')
|
|
57
|
+
.replace(/^[^/]+\//, '')
|
|
58
|
+
.replace(/:\d+:\d+$/, '');
|
|
59
|
+
const file = cleaned.split('/').pop() || cleaned;
|
|
60
|
+
return toComponentLabel(file);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return toComponentLabel(symbol);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const pathMatch = frameLine.match(/\((.*):(\d+):(\d+)\)$/) || frameLine.match(/at\s+(.*):(\d+):(\d+)$/);
|
|
67
|
+
if (pathMatch?.[1]) {
|
|
68
|
+
const rawPath = pathMatch[1];
|
|
69
|
+
const parts = rawPath.split('/');
|
|
70
|
+
return toComponentLabel(parts[parts.length - 1] || rawPath);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return 'Anonymous';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getBestFrame(stack) {
|
|
77
|
+
if (!stack) return null;
|
|
78
|
+
|
|
79
|
+
const lines = stack
|
|
80
|
+
.split('\n')
|
|
81
|
+
.map((line) => line.trim())
|
|
82
|
+
.filter(Boolean);
|
|
83
|
+
|
|
84
|
+
const ignoredMarkers = [
|
|
85
|
+
'ax-react/lifecycle/patch.js',
|
|
86
|
+
'getComponentName',
|
|
87
|
+
'getBestFrame',
|
|
88
|
+
'normalizeFrame',
|
|
89
|
+
'wrapHook',
|
|
90
|
+
'wrapped',
|
|
91
|
+
'emitLifecycleEvent',
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
const candidateLines = lines.filter((line) => {
|
|
95
|
+
if (!line.startsWith('at ')) return false;
|
|
96
|
+
if (ignoredMarkers.some((marker) => line.includes(marker))) return false;
|
|
97
|
+
return true;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const appFrame = candidateLines.find((line) =>
|
|
101
|
+
line.includes('/src/') || line.includes('.jsx') || line.includes('.tsx'),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return appFrame || candidateLines[0] || null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getComponentName() {
|
|
108
|
+
const internals = React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const reactStack = internals?.getCurrentStack?.();
|
|
112
|
+
const frame = getBestFrame(reactStack);
|
|
113
|
+
if (frame) return normalizeFrame(frame);
|
|
114
|
+
} catch {
|
|
115
|
+
// Fallback below.
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const stack = new Error().stack;
|
|
120
|
+
const frame = getBestFrame(stack);
|
|
121
|
+
if (frame) return normalizeFrame(frame);
|
|
122
|
+
} catch {
|
|
123
|
+
// Fall through.
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return 'Anonymous';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* -------------------------------------------------------
|
|
130
|
+
* Hook wrapper utility
|
|
131
|
+
* ----------------------------------------------------- */
|
|
132
|
+
function wrapHook(dispatcher, name, wrapper) {
|
|
133
|
+
if (!dispatcher[name] || dispatcher[name].__AX_WRAPPED__) return;
|
|
134
|
+
|
|
135
|
+
const original = dispatcher[name];
|
|
136
|
+
|
|
137
|
+
function wrapped(...args) {
|
|
138
|
+
return wrapper(original, this, args);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
wrapped.__AX_WRAPPED__ = true;
|
|
142
|
+
wrapped.__AX_ORIGINAL__ = original;
|
|
143
|
+
dispatcher[name] = wrapped;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* -------------------------------------------------------
|
|
147
|
+
* Dispatcher patch (THIS is the critical layer)
|
|
148
|
+
* ----------------------------------------------------- */
|
|
149
|
+
function patchDispatcher() {
|
|
150
|
+
const internals =
|
|
151
|
+
React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
|
|
152
|
+
|
|
153
|
+
if (internals.__AX_DISPATCHER_PATCHED__) return;
|
|
154
|
+
internals.__AX_DISPATCHER_PATCHED__ = true;
|
|
155
|
+
|
|
156
|
+
let currentDispatcher = internals.H;
|
|
157
|
+
|
|
158
|
+
Object.defineProperty(internals, 'H', {
|
|
159
|
+
configurable: true,
|
|
160
|
+
get() {
|
|
161
|
+
const dispatcher = currentDispatcher;
|
|
162
|
+
|
|
163
|
+
if (!dispatcher || dispatcher.__AX_PATCHED__) {
|
|
164
|
+
return dispatcher;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
dispatcher.__AX_PATCHED__ = true;
|
|
168
|
+
|
|
169
|
+
/* ---------------- useRef ---------------- */
|
|
170
|
+
wrapHook(dispatcher, 'useRef', (original, ctx, args) => {
|
|
171
|
+
const component = getComponentName();
|
|
172
|
+
const ref = original.apply(ctx, args);
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
useRefLeakTracker.trackRef(ref, {
|
|
176
|
+
component,
|
|
177
|
+
createdAt: performance.now(),
|
|
178
|
+
});
|
|
179
|
+
emitLifecycleEvent(lifecycleEventName(component, 'React useRef Created'), {
|
|
180
|
+
component,
|
|
181
|
+
hasInitialValue: args[0] !== undefined,
|
|
182
|
+
});
|
|
183
|
+
} catch {
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return ref;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
/* ---------------- useEffect ---------------- */
|
|
190
|
+
wrapHook(dispatcher, 'useEffect', (original, ctx, args) => {
|
|
191
|
+
const component = getComponentName();
|
|
192
|
+
const effect = args[0];
|
|
193
|
+
|
|
194
|
+
args[0] = function wrappedEffect() {
|
|
195
|
+
emitLifecycleEvent(lifecycleEventName(component, 'React useEffect Mounted'), { component });
|
|
196
|
+
const cleanup = effect();
|
|
197
|
+
|
|
198
|
+
if (typeof cleanup === 'function') {
|
|
199
|
+
return function wrappedCleanup() {
|
|
200
|
+
emitLifecycleEvent(lifecycleEventName(component, 'React useEffect Cleanup'), { component });
|
|
201
|
+
return cleanup();
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return cleanup;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
return original.apply(ctx, args);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
/* ---------------- useLayoutEffect ---------------- */
|
|
211
|
+
wrapHook(dispatcher, 'useLayoutEffect', (original, ctx, args) => {
|
|
212
|
+
const component = getComponentName();
|
|
213
|
+
const effect = args[0];
|
|
214
|
+
|
|
215
|
+
args[0] = function wrappedLayoutEffect() {
|
|
216
|
+
emitLifecycleEvent(lifecycleEventName(component, 'React useLayoutEffect Mounted'), { component });
|
|
217
|
+
const cleanup = effect();
|
|
218
|
+
|
|
219
|
+
if (typeof cleanup === 'function') {
|
|
220
|
+
return function wrappedLayoutCleanup() {
|
|
221
|
+
emitLifecycleEvent(lifecycleEventName(component, 'React useLayoutEffect Cleanup'), { component });
|
|
222
|
+
return cleanup();
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return cleanup;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
return original.apply(ctx, args);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
/* ---------------- useState ---------------- */
|
|
233
|
+
wrapHook(dispatcher, 'useState', (original, ctx, args) => {
|
|
234
|
+
const component = getComponentName();
|
|
235
|
+
const [state, setState] = original.apply(ctx, args);
|
|
236
|
+
|
|
237
|
+
const wrappedSetState = (value) => {
|
|
238
|
+
emitLifecycleEvent(lifecycleEventName(component, 'React useState Updated'), {
|
|
239
|
+
component,
|
|
240
|
+
updateType: typeof value,
|
|
241
|
+
});
|
|
242
|
+
return setState(value);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
return [state, wrappedSetState];
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
/* ---------------- useReducer ---------------- */
|
|
249
|
+
wrapHook(dispatcher, 'useReducer', (original, ctx, args) => {
|
|
250
|
+
const component = getComponentName();
|
|
251
|
+
|
|
252
|
+
const [state, dispatch] = original.apply(ctx, args);
|
|
253
|
+
|
|
254
|
+
const wrappedDispatch = (action) => {
|
|
255
|
+
emitLifecycleEvent(lifecycleEventName(component, 'React useReducer Dispatched'), {
|
|
256
|
+
component,
|
|
257
|
+
actionType: typeof action,
|
|
258
|
+
});
|
|
259
|
+
return dispatch(action);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
return [state, wrappedDispatch];
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
/* ---------------- useMemo ---------------- */
|
|
266
|
+
wrapHook(dispatcher, 'useMemo', (original, ctx, args) => {
|
|
267
|
+
const component = getComponentName();
|
|
268
|
+
emitLifecycleEvent(lifecycleEventName(component, 'React useMemo Evaluated'), { component });
|
|
269
|
+
return original.apply(ctx, args);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
/* ---------------- useCallback ---------------- */
|
|
273
|
+
wrapHook(dispatcher, 'useCallback', (original, ctx, args) => {
|
|
274
|
+
const component = getComponentName();
|
|
275
|
+
emitLifecycleEvent(lifecycleEventName(component, 'React useCallback Registered'), { component });
|
|
276
|
+
return original.apply(ctx, args);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
return dispatcher;
|
|
280
|
+
},
|
|
281
|
+
set(v) {
|
|
282
|
+
currentDispatcher = v;
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/* -------------------------------------------------------
|
|
288
|
+
* Public init API
|
|
289
|
+
* ----------------------------------------------------- */
|
|
290
|
+
if (!React.__AX_REACT_PATCHED__) {
|
|
291
|
+
|
|
292
|
+
React.__AX_REACT_PATCHED__ = true;
|
|
293
|
+
|
|
294
|
+
patchDispatcher();
|
|
295
|
+
|
|
296
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { LeakTracker } from '@appxiom/core';
|
|
2
|
+
|
|
3
|
+
class UseRefLeakTracker extends LeakTracker {
|
|
4
|
+
// Constructor
|
|
5
|
+
constructor() {
|
|
6
|
+
super(2); // useRef leak type
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
trackRef(ref, metadata = {}) {
|
|
10
|
+
if (!ref) return;
|
|
11
|
+
this.trackInstance(ref, metadata);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const useRefLeakTracker = new UseRefLeakTracker();
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@appxiom/4react",
|
|
3
|
+
"description": "React integration layer for Appxiom Web Observability SDK",
|
|
4
|
+
"license": "UNLICENSED",
|
|
5
|
+
"author": "BasilGregory Software Labs Private Limited",
|
|
6
|
+
"homepage": "https://appxiom.io",
|
|
7
|
+
"version": "0.1.6",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=18"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./index.d.ts",
|
|
17
|
+
"import": "./index.js"
|
|
18
|
+
},
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"types": "./index.d.ts",
|
|
22
|
+
"files": [
|
|
23
|
+
"index.js",
|
|
24
|
+
"index.d.ts",
|
|
25
|
+
"core/",
|
|
26
|
+
"lifecycle/",
|
|
27
|
+
"memory/",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"check:files": "test -f index.js && test -f core/ax-react.js && test -f lifecycle/patch.js && test -f memory/ax-react-useref-leak.js",
|
|
33
|
+
"check:types": "tsc --project test/types/tsconfig.json --noEmit",
|
|
34
|
+
"pack:dry-run": "npm pack --dry-run",
|
|
35
|
+
"prepublishOnly": "npm run check:files && npm run pack:dry-run"
|
|
36
|
+
},
|
|
37
|
+
"sideEffects": true,
|
|
38
|
+
"main": "index.js",
|
|
39
|
+
"module": "index.js",
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@appxiom/core": "^0.1.6"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"typescript": "^5.9.3"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"react": ">=18"
|
|
48
|
+
},
|
|
49
|
+
"keywords": [
|
|
50
|
+
"appxiom",
|
|
51
|
+
"react",
|
|
52
|
+
"observability",
|
|
53
|
+
"monitoring",
|
|
54
|
+
"analytics",
|
|
55
|
+
"rum",
|
|
56
|
+
"performance"
|
|
57
|
+
],
|
|
58
|
+
"type": "module"
|
|
59
|
+
}
|