@refraction-ui/astro 0.5.1 → 0.6.0
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/dist/analytics/analytics-manager.ts +361 -0
- package/dist/analytics/consent.ts +40 -0
- package/dist/analytics/console-sink.ts +37 -0
- package/dist/analytics/http-sink.ts +213 -0
- package/dist/analytics/identity.ts +64 -0
- package/dist/analytics/index.ts +60 -0
- package/dist/analytics/mock-sink.ts +62 -0
- package/dist/analytics/noop.ts +48 -0
- package/dist/analytics/redaction.ts +93 -0
- package/dist/analytics/session.ts +162 -0
- package/dist/analytics/storage.ts +119 -0
- package/dist/analytics/types.ts +273 -0
- package/dist/analytics/uuid.ts +63 -0
- package/dist/astro-analytics/AnalyticsScript.astro +130 -0
- package/dist/astro-analytics/index.ts +49 -0
- package/dist/astro-logger/TelemetryScript.astro +108 -0
- package/dist/astro-logger/index.ts +48 -0
- package/dist/astro-logger/library-error-capture.ts +121 -0
- package/dist/astro-logger/telemetry-middleware.ts +92 -0
- package/dist/index.ts +2 -0
- package/dist/logger/console-sink.ts +64 -0
- package/dist/logger/faro-engine.ts +130 -0
- package/dist/logger/index.ts +39 -0
- package/dist/logger/mock-sink.ts +38 -0
- package/dist/logger/noop.ts +52 -0
- package/dist/logger/presets.ts +51 -0
- package/dist/logger/redact.ts +33 -0
- package/dist/logger/telemetry-manager.ts +229 -0
- package/dist/logger/types.ts +121 -0
- package/dist/shared/dev-feedback.ts +154 -0
- package/dist/shared/index.ts +25 -0
- package/dist/shared/library-origin-error.ts +240 -0
- package/package.json +4 -2
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* library-origin-error — builds the **library-origin error envelope** for
|
|
3
|
+
* errors that originate inside a `@refraction-ui/*` package.
|
|
4
|
+
*
|
|
5
|
+
* The envelope is expressed THROUGH the existing telemetry record contract
|
|
6
|
+
* (`@refraction-ui/logger`'s `LogRecord`) — the contract is reused, NOT
|
|
7
|
+
* redefined. To keep `@refraction-ui/shared` zero-dependency, the record is
|
|
8
|
+
* mirrored structurally (see {@link DevFeedbackRecord}) rather than imported;
|
|
9
|
+
* a consumer wiring the real logger sink satisfies it structurally.
|
|
10
|
+
*
|
|
11
|
+
* Aggressive redaction (epic #247 guardrails): the payload is package,
|
|
12
|
+
* componentName, version, a normalized stack **fingerprint hash**, and
|
|
13
|
+
* framework only. Never app state, props values, user data, PII, or full app
|
|
14
|
+
* stack frames.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { DevFeedbackRecord, DevFeedbackSink } from './dev-feedback.js'
|
|
18
|
+
|
|
19
|
+
/** Matches any stack frame that references a `@refraction-ui/*` package. */
|
|
20
|
+
const REFRACTION_FRAME = /@refraction-ui\//
|
|
21
|
+
|
|
22
|
+
/** Frameworks a refraction-ui adapter can run under. */
|
|
23
|
+
export type LibraryFramework =
|
|
24
|
+
| 'react'
|
|
25
|
+
| 'angular'
|
|
26
|
+
| 'astro'
|
|
27
|
+
| 'vue'
|
|
28
|
+
| 'svelte'
|
|
29
|
+
| 'vanilla'
|
|
30
|
+
| 'unknown'
|
|
31
|
+
|
|
32
|
+
/** Inputs describing where a library-origin error came from. */
|
|
33
|
+
export interface LibraryOriginErrorInput {
|
|
34
|
+
/** Originating package, e.g. `@refraction-ui/react`. */
|
|
35
|
+
package: string
|
|
36
|
+
/** refraction-ui component the error originated in, e.g. `Dialog`. */
|
|
37
|
+
componentName: string
|
|
38
|
+
/** Originating package version, e.g. `0.1.5`. */
|
|
39
|
+
version: string
|
|
40
|
+
/** Host framework the component was running under. */
|
|
41
|
+
framework: LibraryFramework
|
|
42
|
+
/** The thrown error (or its stack string). Used ONLY to derive a hash. */
|
|
43
|
+
error?: unknown
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The redacted, library-origin payload carried in `LogRecord.context`. Contains
|
|
48
|
+
* NO app data — only the five identifying fields plus a stack fingerprint hash.
|
|
49
|
+
*/
|
|
50
|
+
export interface LibraryOriginEnvelope {
|
|
51
|
+
/** Marks this record as a library-origin error (for downstream routing). */
|
|
52
|
+
origin: 'refraction-ui'
|
|
53
|
+
package: string
|
|
54
|
+
componentName: string
|
|
55
|
+
version: string
|
|
56
|
+
framework: LibraryFramework
|
|
57
|
+
/** Stable, app-data-free hash of the normalized stack. */
|
|
58
|
+
fingerprint: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Normalize a stack trace into a deterministic, app-data-free string before
|
|
63
|
+
* hashing:
|
|
64
|
+
* - keep only frames that reference a `@refraction-ui/*` package (drop the
|
|
65
|
+
* app's own frames — library-origin only),
|
|
66
|
+
* - strip absolute paths, line/column numbers, query strings, and hashes,
|
|
67
|
+
* - sort-free (preserve call order) but whitespace-collapsed.
|
|
68
|
+
*
|
|
69
|
+
* If no refraction-ui frames are present the function returns an empty string,
|
|
70
|
+
* which yields a stable "no-frames" fingerprint rather than leaking app frames.
|
|
71
|
+
*/
|
|
72
|
+
function normalizeStack(stack: string): string {
|
|
73
|
+
const frames = stack
|
|
74
|
+
.split('\n')
|
|
75
|
+
.map((line) => line.trim())
|
|
76
|
+
.filter((line) => line.startsWith('at ') || line.includes('@'))
|
|
77
|
+
.filter((line) => REFRACTION_FRAME.test(line))
|
|
78
|
+
.map((line) =>
|
|
79
|
+
line
|
|
80
|
+
// drop everything from the path/url onward, keep the symbol
|
|
81
|
+
.replace(/\((?:.*?)(@refraction-ui\/[^):]+)[^)]*\)/, '($1)')
|
|
82
|
+
.replace(/(@refraction-ui\/[^\s:?#]+)[^\s)]*/g, '$1')
|
|
83
|
+
// strip :line:col
|
|
84
|
+
.replace(/:\d+:\d+/g, '')
|
|
85
|
+
// strip line:col without colon prefix
|
|
86
|
+
.replace(/:\d+\b/g, '')
|
|
87
|
+
// strip query/hash
|
|
88
|
+
.replace(/[?#][^\s)]*/g, '')
|
|
89
|
+
.replace(/\s+/g, ' ')
|
|
90
|
+
.trim(),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return frames.join('\n')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Deterministic, dependency-free 53-bit string hash (cyrb53). Stable across
|
|
98
|
+
* runs and processes for the same input — required so the intake can dedupe by
|
|
99
|
+
* fingerprint. NOT cryptographic; it only needs to be collision-resistant
|
|
100
|
+
* enough to group identical normalized stacks.
|
|
101
|
+
*/
|
|
102
|
+
function cyrb53(str: string, seed = 0): string {
|
|
103
|
+
let h1 = 0xdeadbeef ^ seed
|
|
104
|
+
let h2 = 0x41c6ce57 ^ seed
|
|
105
|
+
for (let i = 0; i < str.length; i++) {
|
|
106
|
+
const ch = str.charCodeAt(i)
|
|
107
|
+
h1 = Math.imul(h1 ^ ch, 2654435761)
|
|
108
|
+
h2 = Math.imul(h2 ^ ch, 1597334677)
|
|
109
|
+
}
|
|
110
|
+
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507)
|
|
111
|
+
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909)
|
|
112
|
+
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507)
|
|
113
|
+
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909)
|
|
114
|
+
const out = 4294967296 * (2097151 & h2) + (h1 >>> 0)
|
|
115
|
+
return out.toString(16).padStart(14, '0')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function stackOf(error: unknown): string {
|
|
119
|
+
if (typeof error === 'string') return error
|
|
120
|
+
if (
|
|
121
|
+
error &&
|
|
122
|
+
typeof error === 'object' &&
|
|
123
|
+
'stack' in error &&
|
|
124
|
+
typeof (error as { stack?: unknown }).stack === 'string'
|
|
125
|
+
) {
|
|
126
|
+
return (error as { stack: string }).stack
|
|
127
|
+
}
|
|
128
|
+
return ''
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Compute the stable stack fingerprint for a library-origin error. Exposed
|
|
133
|
+
* separately so capture seams / tests can assert fingerprint stability without
|
|
134
|
+
* building the whole record.
|
|
135
|
+
*/
|
|
136
|
+
export function stackFingerprint(error: unknown): string {
|
|
137
|
+
const normalized = normalizeStack(stackOf(error))
|
|
138
|
+
return cyrb53(normalized)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Build the redacted library-origin envelope for an error.
|
|
143
|
+
*/
|
|
144
|
+
export function libraryOriginEnvelope(
|
|
145
|
+
input: LibraryOriginErrorInput,
|
|
146
|
+
): LibraryOriginEnvelope {
|
|
147
|
+
return {
|
|
148
|
+
origin: 'refraction-ui',
|
|
149
|
+
package: input.package,
|
|
150
|
+
componentName: input.componentName,
|
|
151
|
+
version: input.version,
|
|
152
|
+
framework: input.framework,
|
|
153
|
+
fingerprint: stackFingerprint(input.error),
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Build the full library-origin error record, expressed THROUGH the existing
|
|
159
|
+
* telemetry record contract (`LogRecord`, mirrored as {@link DevFeedbackRecord}
|
|
160
|
+
* to avoid a hard logger dependency). The redacted envelope rides in
|
|
161
|
+
* `context`; no app data, props, PII, or app stack frames are included.
|
|
162
|
+
*
|
|
163
|
+
* This record is exactly what a consumer-wired telemetry sink (or the
|
|
164
|
+
* `devWarn`/`devError` injected sink) consumes.
|
|
165
|
+
*/
|
|
166
|
+
export function libraryOriginError(
|
|
167
|
+
input: LibraryOriginErrorInput,
|
|
168
|
+
): DevFeedbackRecord {
|
|
169
|
+
const envelope = libraryOriginEnvelope(input)
|
|
170
|
+
return {
|
|
171
|
+
level: 'error',
|
|
172
|
+
message: `${input.package}/${input.componentName}: library-origin error`,
|
|
173
|
+
timestamp: Date.now(),
|
|
174
|
+
context: { ...envelope },
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Library-origin predicate — `true` iff the error's stack contains at least
|
|
180
|
+
* one frame that references a `@refraction-ui/*` package. This is the gate
|
|
181
|
+
* every per-framework capture seam uses: only library-origin errors are
|
|
182
|
+
* eligible; the app's own errors return `false` and pass through untouched.
|
|
183
|
+
*
|
|
184
|
+
* Mirrors exactly the frame-detection rule {@link stackFingerprint} normalizes
|
|
185
|
+
* by — no new contract, just the companion predicate the seams need. Never
|
|
186
|
+
* throws for a missing/odd error (returns `false`).
|
|
187
|
+
*/
|
|
188
|
+
export function isLibraryOriginError(error: unknown): boolean {
|
|
189
|
+
const stack = stackOf(error)
|
|
190
|
+
if (!stack) return false
|
|
191
|
+
return REFRACTION_FRAME.test(stack)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The fixed identity of the refraction-ui package/component a seam tags an
|
|
196
|
+
* error with. The seam supplies this; the stack itself supplies the
|
|
197
|
+
* fingerprint. Never carries app data.
|
|
198
|
+
*/
|
|
199
|
+
export type LibraryOriginIdentity = Pick<
|
|
200
|
+
LibraryOriginErrorInput,
|
|
201
|
+
'package' | 'componentName' | 'version' | 'framework'
|
|
202
|
+
>
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The single capture primitive shared by every per-framework seam (React error
|
|
206
|
+
* boundary, Angular `ErrorHandler`, Astro middleware). It performs the entire
|
|
207
|
+
* guarded flow in one place so the seams stay thin and identical in behavior:
|
|
208
|
+
*
|
|
209
|
+
* 1. **Library-origin filter** — if the error's stack has no
|
|
210
|
+
* `@refraction-ui/*` frame it is the app's own error: return `null` and do
|
|
211
|
+
* NOT touch, capture, or forward it.
|
|
212
|
+
* 2. **Tag** — build the redacted {@link libraryOriginError} record
|
|
213
|
+
* (package/componentName/version/framework + app-data-free fingerprint).
|
|
214
|
+
* 3. **Route to the optional sink** — forward the record to the
|
|
215
|
+
* consumer-injected sink ONLY if one was wired; a broken sink can never
|
|
216
|
+
* break the consumer app. With no sink this is a no-op beyond returning
|
|
217
|
+
* the record (so a seam can still surface it locally).
|
|
218
|
+
*
|
|
219
|
+
* @returns the tagged record for a library-origin error, or `null` when the
|
|
220
|
+
* error is app-origin (the "pass through untouched" signal).
|
|
221
|
+
*/
|
|
222
|
+
export function captureLibraryOriginError(
|
|
223
|
+
error: unknown,
|
|
224
|
+
identity: LibraryOriginIdentity,
|
|
225
|
+
sink: DevFeedbackSink | null | undefined,
|
|
226
|
+
): DevFeedbackRecord | null {
|
|
227
|
+
if (!isLibraryOriginError(error)) return null
|
|
228
|
+
|
|
229
|
+
const record = libraryOriginError({ ...identity, error })
|
|
230
|
+
|
|
231
|
+
if (sink) {
|
|
232
|
+
try {
|
|
233
|
+
sink.log(record)
|
|
234
|
+
} catch {
|
|
235
|
+
// A broken sink must never break the consumer app.
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return record
|
|
240
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@refraction-ui/astro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "All Refraction UI Astro components in one package",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -93,7 +93,9 @@
|
|
|
93
93
|
"@refraction-ui/astro-link-card": "workspace:*",
|
|
94
94
|
"@refraction-ui/astro-card-grid": "workspace:*",
|
|
95
95
|
"@refraction-ui/astro-payment": "workspace:*",
|
|
96
|
-
"@refraction-ui/astro-command-input": "workspace:*"
|
|
96
|
+
"@refraction-ui/astro-command-input": "workspace:*",
|
|
97
|
+
"@refraction-ui/astro-logger": "workspace:*",
|
|
98
|
+
"@refraction-ui/astro-analytics": "workspace:*"
|
|
97
99
|
},
|
|
98
100
|
"publishConfig": {
|
|
99
101
|
"access": "public"
|