@universelle/sentinel-report 0.1.0 → 0.3.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/CHANGELOG.md +28 -0
- package/README.md +49 -0
- package/dist/core.d.ts +23 -3
- package/dist/core.js +74 -11
- package/dist/element.d.ts +11 -0
- package/dist/element.js +21 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/logo.d.ts +8 -0
- package/dist/logo.js +14 -0
- package/dist/mark.d.ts +1 -0
- package/dist/mark.js +3 -1
- package/dist/react.d.ts +42 -3
- package/dist/react.js +11 -2
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog — @universelle/sentinel-report
|
|
2
|
+
|
|
3
|
+
## 0.3.0 — 2026-10-24
|
|
4
|
+
|
|
5
|
+
### Privacy
|
|
6
|
+
For sites whose paths carry secrets (an invitation token in `/u/<token>`, a personal link, internal ids), three
|
|
7
|
+
options in `createReporter`, `useSentinelReport` and `capture()` clean everything that leaves the page. Recording
|
|
8
|
+
stays raw in memory; the cleaning happens when the context is built (`context()`, `send()`):
|
|
9
|
+
|
|
10
|
+
- **`route(pathname) => template`**. Applies to `page.path`, to every entry of `trail` and to every `network[].path`.
|
|
11
|
+
It always receives the path without its query string or fragment. If it throws or returns something that isn't a
|
|
12
|
+
string, that path goes as `"/(otra)"`, never raw. Consecutive pages with the same template become one entry.
|
|
13
|
+
- **The title.** With `route`, `page.title` goes as `""`, because the title can carry the page's data. `title: true`
|
|
14
|
+
sends it anyway.
|
|
15
|
+
- **Console messages.** With `route`, every absolute URL becomes its origin (`https://host/`). Every path at the start
|
|
16
|
+
of the message, or after a space, a bracket or a quote, goes through `route`. Then **`redact(message)`**, if given,
|
|
17
|
+
for anything else (UUIDs, tokens…). If `redact` throws, the message goes empty.
|
|
18
|
+
- **Without these options nothing changes**: the body is the same as in 0.2.0.
|
|
19
|
+
|
|
20
|
+
Unchanged: the body's format, the endpoint, `credentials: "omit"`, the limit of 20 entries, no dependencies and no
|
|
21
|
+
install scripts.
|
|
22
|
+
|
|
23
|
+
## 0.2.0 — 2026-10-24
|
|
24
|
+
- SENTINEL's mark for your own branding: `<SentinelMark>`, `<sentinel-mark>`, `markSvg()` and `MARK_SHAPES`.
|
|
25
|
+
|
|
26
|
+
## 0.1.0 — 2026-10-24
|
|
27
|
+
- First release: the core (`capture`, `createReporter`), the `<sentinel-report>` web component and React
|
|
28
|
+
(`<ReportButton>`, `useSentinelReport`).
|
package/README.md
CHANGED
|
@@ -125,6 +125,55 @@ import { createReporter } from "@universelle/sentinel-report";
|
|
|
125
125
|
await createReporter({ site: "your-site" }).send({ kind: "wish", text: "Export the table" });
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
+
## Privacy: paths with secrets
|
|
129
|
+
|
|
130
|
+
If your paths carry secrets (an invitation token in `/u/<token>`, a personal link, internal ids), pass `route`. It
|
|
131
|
+
turns each real path into its template, so SENTINEL only receives templates. It applies to the page, every page of
|
|
132
|
+
the trail, every failed request and the paths and URLs inside console messages, and the page's title is dropped.
|
|
133
|
+
`redact` cleans console messages further.
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
const route = (pathname: string) => toTemplate(pathname) ?? "/(otra)"; // e.g. "/u/[invitation]"
|
|
137
|
+
const redact = (message: string) => message.replace(UUID, "[id]");
|
|
138
|
+
|
|
139
|
+
const { send } = useSentinelReport({ site: "your-site", route, redact });
|
|
140
|
+
// or: createReporter({ site: "your-site", route, redact }); or once for the page: capture({ route, redact })
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
A `route` that throws, or doesn't return a string, sends `"/(otra)"`, never the raw path. Without these options,
|
|
144
|
+
everything is sent as in 0.2.0. See [CHANGELOG.md](CHANGELOG.md).
|
|
145
|
+
|
|
146
|
+
## Branding: SENTINEL's mark
|
|
147
|
+
|
|
148
|
+
For your own button, a footer or a "Protected by SENTINEL" line. The eye and the star take the text colour
|
|
149
|
+
(`currentColor`), so the mark works on light and dark backgrounds; the moon is always the brand's green.
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
import { SentinelMark, useSentinelReport } from "@universelle/sentinel-report/react";
|
|
153
|
+
|
|
154
|
+
<button onClick={…}>
|
|
155
|
+
<SentinelMark size={20} /> Report a problem
|
|
156
|
+
</button>
|
|
157
|
+
|
|
158
|
+
// title makes it an image with a name, for screen readers:
|
|
159
|
+
<SentinelMark size={32} title="SENTINEL" />
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Without React:
|
|
163
|
+
|
|
164
|
+
```html
|
|
165
|
+
<script type="module">import "@universelle/sentinel-report/element";</script>
|
|
166
|
+
<span style="color: #6b3fc4">Made with <sentinel-mark size="20" label="SENTINEL"></sentinel-mark></span>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
```js
|
|
170
|
+
import { markSvg, MARK_SHAPES } from "@universelle/sentinel-report";
|
|
171
|
+
|
|
172
|
+
button.innerHTML = `${markSvg({ size: 20 })} Report`;
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`markSvg` gives you the `<svg>` as a string. `MARK_SHAPES` gives you its inside, for your own `<svg viewBox="0 0 100 100">`.
|
|
176
|
+
|
|
128
177
|
## Notes
|
|
129
178
|
|
|
130
179
|
- **Content Security Policy:** allow `connect-src https://sentinel.cervilla.es`, plus `script-src` if you use the
|
package/dist/core.d.ts
CHANGED
|
@@ -30,6 +30,17 @@ export type Report = {
|
|
|
30
30
|
text: string;
|
|
31
31
|
email?: string;
|
|
32
32
|
};
|
|
33
|
+
/** How a site cleans what is sent (0.3.0). Without `route`, everything goes as in 0.2.0. */
|
|
34
|
+
export type Privacy = {
|
|
35
|
+
/** A real path (`/u/AbC…`) to its template (`/u/[invitation]`). If it throws or returns no string: `"/(otra)"`. */
|
|
36
|
+
route?: (pathname: string) => string;
|
|
37
|
+
/** Every console message, after the default cleaning (with `route`: URLs to their origin, paths to templates). */
|
|
38
|
+
redact?: (message: string) => string;
|
|
39
|
+
/** Send `document.title` (default: yes without `route`, no with it: the title can carry the page's data). */
|
|
40
|
+
title?: boolean;
|
|
41
|
+
};
|
|
42
|
+
/** What goes in place of a path the site's `route` can't turn into a template. */
|
|
43
|
+
export declare const UNKNOWN_ROUTE = "/(otra)";
|
|
33
44
|
export type Reporter = {
|
|
34
45
|
/** Sends a report; rejects with an `Error` whose message is the HTTP status (or the network error). */
|
|
35
46
|
send(report: Report): Promise<void>;
|
|
@@ -41,16 +52,25 @@ type Capture = {
|
|
|
41
52
|
network: FailedRequest[];
|
|
42
53
|
trail: string[];
|
|
43
54
|
fetch: typeof fetch;
|
|
55
|
+
privacy: Privacy;
|
|
44
56
|
};
|
|
45
57
|
declare global {
|
|
46
58
|
interface Window {
|
|
47
59
|
__sentinelCapture?: Capture;
|
|
48
60
|
}
|
|
49
61
|
}
|
|
50
|
-
/**
|
|
51
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Starts recording the visit's context (once per page; later calls return the same recording). The privacy options
|
|
64
|
+
* given here are the page's default for every reporter that doesn't bring its own; a later call can still set them.
|
|
65
|
+
*/
|
|
66
|
+
export declare function capture(privacy?: Privacy): Capture;
|
|
67
|
+
/**
|
|
68
|
+
* A console message as it may leave: with `route`, each absolute URL becomes its origin and each path (at the start,
|
|
69
|
+
* or after a space, a bracket or a quote) its template; then the site's own `redact`, if any.
|
|
70
|
+
*/
|
|
71
|
+
export declare function cleanMessage(message: string, privacy: Privacy): string;
|
|
52
72
|
export declare function createReporter(options: {
|
|
53
73
|
site: string;
|
|
54
74
|
endpoint?: string;
|
|
55
|
-
}): Reporter;
|
|
75
|
+
} & Privacy): Reporter;
|
|
56
76
|
export {};
|
package/dist/core.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
// The core, with no framework: it records the technical context of the visit (console errors, failed requests,
|
|
2
2
|
// pages visited) and sends a report to SENTINEL. What it never sends: the query string or fragment of any URL (they
|
|
3
3
|
// can carry tokens) or anything typed into the site. Installed once per page, whoever asks first.
|
|
4
|
+
//
|
|
5
|
+
// Privacy (0.3.0): sites whose paths carry secrets (`/u/<invitation token>`) pass `route`, which turns a real path
|
|
6
|
+
// into its template (`/u/[invitation]`). What is recorded stays raw in memory; everything that leaves (`context()`,
|
|
7
|
+
// `send()`) goes through it: the page, every page of the trail, every failed request, and the paths and URLs inside
|
|
8
|
+
// console messages; and the page's title is dropped. `redact` cleans console messages further.
|
|
4
9
|
export const DEFAULT_ENDPOINT = "https://sentinel.cervilla.es";
|
|
10
|
+
/** What goes in place of a path the site's `route` can't turn into a template. */
|
|
11
|
+
export const UNKNOWN_ROUTE = "/(otra)";
|
|
5
12
|
const MAX = 20;
|
|
6
13
|
function clip(text) {
|
|
7
14
|
return String(text).replace(/\s+/g, " ").slice(0, 300);
|
|
@@ -74,27 +81,83 @@ function watchNavigation(capture) {
|
|
|
74
81
|
}
|
|
75
82
|
window.addEventListener("popstate", visit);
|
|
76
83
|
}
|
|
77
|
-
/**
|
|
78
|
-
|
|
79
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Starts recording the visit's context (once per page; later calls return the same recording). The privacy options
|
|
86
|
+
* given here are the page's default for every reporter that doesn't bring its own; a later call can still set them.
|
|
87
|
+
*/
|
|
88
|
+
export function capture(privacy = {}) {
|
|
89
|
+
if (window.__sentinelCapture !== undefined) {
|
|
90
|
+
if (privacy.route !== undefined || privacy.redact !== undefined || privacy.title !== undefined)
|
|
91
|
+
window.__sentinelCapture.privacy = privacy;
|
|
80
92
|
return window.__sentinelCapture;
|
|
81
|
-
|
|
93
|
+
}
|
|
94
|
+
const recording = { logs: [], network: [], trail: [location.pathname], fetch: window.fetch.bind(window), privacy };
|
|
82
95
|
window.__sentinelCapture = recording;
|
|
83
96
|
watchConsole(recording);
|
|
84
97
|
watchFetch(recording);
|
|
85
98
|
watchNavigation(recording);
|
|
86
99
|
return recording;
|
|
87
100
|
}
|
|
101
|
+
/** A path through the site's `route`, never raw once a route is given: what fails goes as `"/(otra)"`. */
|
|
102
|
+
function template(route, pathname) {
|
|
103
|
+
if (route === undefined)
|
|
104
|
+
return pathname;
|
|
105
|
+
try {
|
|
106
|
+
const mapped = route(pathname.split(/[?#]/)[0]);
|
|
107
|
+
return typeof mapped === "string" ? mapped : UNKNOWN_ROUTE;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return UNKNOWN_ROUTE;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* A console message as it may leave: with `route`, each absolute URL becomes its origin and each path (at the start,
|
|
115
|
+
* or after a space, a bracket or a quote) its template; then the site's own `redact`, if any.
|
|
116
|
+
*/
|
|
117
|
+
export function cleanMessage(message, privacy) {
|
|
118
|
+
let clean = message;
|
|
119
|
+
if (privacy.route !== undefined) {
|
|
120
|
+
clean = clean.replace(/https?:\/\/[^\s"'()<>]+/g, (url) => {
|
|
121
|
+
try {
|
|
122
|
+
return `${new URL(url).origin}/`;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return "[url]";
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
clean = clean.replace(/(^|[\s("'])(\/[^\s"'()<>]*)/g, (_match, before, pathname) => `${before}${template(privacy.route, pathname)}`);
|
|
129
|
+
}
|
|
130
|
+
if (privacy.redact !== undefined) {
|
|
131
|
+
try {
|
|
132
|
+
const redacted = privacy.redact(clean);
|
|
133
|
+
clean = typeof redacted === "string" ? redacted : "";
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
clean = "";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return clean;
|
|
140
|
+
}
|
|
141
|
+
/** The trail through `route`; consecutive pages with the same template become one. */
|
|
142
|
+
function trailOf(trail, route) {
|
|
143
|
+
return trail.map((pathname) => template(route, pathname)).filter((pathname, index, all) => index === 0 || all[index - 1] !== pathname);
|
|
144
|
+
}
|
|
88
145
|
export function createReporter(options) {
|
|
89
146
|
const recording = capture();
|
|
90
147
|
const url = `${(options.endpoint ?? DEFAULT_ENDPOINT).replace(/\/+$/, "")}/reports/${encodeURIComponent(options.site)}`;
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
148
|
+
const own = options.route !== undefined || options.redact !== undefined || options.title !== undefined;
|
|
149
|
+
const context = () => {
|
|
150
|
+
// The reporter's own options, else the page's default (from `capture()`), read when sending.
|
|
151
|
+
const privacy = own ? { route: options.route, redact: options.redact, title: options.title } : recording.privacy;
|
|
152
|
+
const withTitle = privacy.title ?? privacy.route === undefined;
|
|
153
|
+
return {
|
|
154
|
+
page: { path: template(privacy.route, location.pathname), title: withTitle ? document.title.slice(0, 200) : "" },
|
|
155
|
+
device: { userAgent: navigator.userAgent.slice(0, 300), language: navigator.language, viewport: `${innerWidth}x${innerHeight}`, screen: `${screen.width}x${screen.height}` },
|
|
156
|
+
console: recording.logs.map((entry) => ({ ...entry, message: cleanMessage(entry.message, privacy) })),
|
|
157
|
+
network: recording.network.map((entry) => ({ ...entry, path: template(privacy.route, entry.path) })),
|
|
158
|
+
trail: privacy.route === undefined ? recording.trail.slice() : trailOf(recording.trail, privacy.route),
|
|
159
|
+
};
|
|
160
|
+
};
|
|
98
161
|
return {
|
|
99
162
|
context,
|
|
100
163
|
async send(report) {
|
package/dist/element.d.ts
CHANGED
|
@@ -34,8 +34,19 @@ export declare class SentinelReport extends HTMLElement {
|
|
|
34
34
|
private say;
|
|
35
35
|
private submit;
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* `<sentinel-mark size="24" label="…">`: SENTINEL's mark on its own, for your branding without React. It takes the
|
|
39
|
+
* text colour (`color` on the element); `label` makes it an image with that name, otherwise it is decorative.
|
|
40
|
+
*/
|
|
41
|
+
export declare class SentinelMark extends HTMLElement {
|
|
42
|
+
static observedAttributes: string[];
|
|
43
|
+
connectedCallback(): void;
|
|
44
|
+
attributeChangedCallback(): void;
|
|
45
|
+
private render;
|
|
46
|
+
}
|
|
37
47
|
declare global {
|
|
38
48
|
interface HTMLElementTagNameMap {
|
|
39
49
|
"sentinel-report": SentinelReport;
|
|
50
|
+
"sentinel-mark": SentinelMark;
|
|
40
51
|
}
|
|
41
52
|
}
|
package/dist/element.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// (auto | light | dark), hide-mark; and the `texts` property for every other word.
|
|
9
9
|
// Events: `sentinel-sent` (detail: { kind }) and `sentinel-error` (detail: { error }). Methods: open(), close().
|
|
10
10
|
import { capture, createReporter } from "./core.js";
|
|
11
|
+
import { markSvg } from "./logo.js";
|
|
11
12
|
import { MARK } from "./mark.js";
|
|
12
13
|
export const DEFAULT_TEXTS = {
|
|
13
14
|
label: "¿Algo falla?",
|
|
@@ -159,5 +160,25 @@ export class SentinelReport extends HTMLElement {
|
|
|
159
160
|
}
|
|
160
161
|
}
|
|
161
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* `<sentinel-mark size="24" label="…">`: SENTINEL's mark on its own, for your branding without React. It takes the
|
|
165
|
+
* text colour (`color` on the element); `label` makes it an image with that name, otherwise it is decorative.
|
|
166
|
+
*/
|
|
167
|
+
export class SentinelMark extends HTMLElement {
|
|
168
|
+
static observedAttributes = ["size", "label"];
|
|
169
|
+
connectedCallback() {
|
|
170
|
+
this.render();
|
|
171
|
+
}
|
|
172
|
+
attributeChangedCallback() {
|
|
173
|
+
if (this.isConnected)
|
|
174
|
+
this.render();
|
|
175
|
+
}
|
|
176
|
+
render() {
|
|
177
|
+
this.style.display ||= "inline-flex";
|
|
178
|
+
this.innerHTML = markSvg({ size: this.getAttribute("size") ?? "1em", title: this.getAttribute("label") ?? undefined });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
162
181
|
if (typeof customElements !== "undefined" && customElements.get("sentinel-report") === undefined)
|
|
163
182
|
customElements.define("sentinel-report", SentinelReport);
|
|
183
|
+
if (typeof customElements !== "undefined" && customElements.get("sentinel-mark") === undefined)
|
|
184
|
+
customElements.define("sentinel-mark", SentinelMark);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export { capture, createReporter, DEFAULT_ENDPOINT } from "./core.js";
|
|
2
|
-
export
|
|
1
|
+
export { capture, cleanMessage, createReporter, DEFAULT_ENDPOINT, UNKNOWN_ROUTE } from "./core.js";
|
|
2
|
+
export { MARK_SHAPES, markSvg } from "./logo.js";
|
|
3
|
+
export type { Context, ConsoleEntry, FailedRequest, Privacy, Report, ReportKind, Reporter } from "./core.js";
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
// @universelle/sentinel-report: the core, with no framework. For the ready-made button, `…/element` (any site) or
|
|
2
2
|
// `…/react`; to build your own form, `createReporter` here or `useSentinelReport` in React.
|
|
3
|
-
export { capture, createReporter, DEFAULT_ENDPOINT } from "./core.js";
|
|
3
|
+
export { capture, cleanMessage, createReporter, DEFAULT_ENDPOINT, UNKNOWN_ROUTE } from "./core.js";
|
|
4
|
+
export { MARK_SHAPES, markSvg } from "./logo.js";
|
package/dist/logo.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { MARK_SHAPES } from "./mark.js";
|
|
2
|
+
export { MARK_SHAPES };
|
|
3
|
+
/** The mark as an `<svg>`: decorative by default; with `title`, an image with that name for screen readers. */
|
|
4
|
+
export declare function markSvg(options?: {
|
|
5
|
+
size?: number | string;
|
|
6
|
+
title?: string;
|
|
7
|
+
className?: string;
|
|
8
|
+
}): string;
|
package/dist/logo.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// SENTINEL's mark for your own branding (a button of your own, a footer…): as an SVG string. The eye and the star take
|
|
2
|
+
// the text colour (currentColor), so it works on light and dark backgrounds; the moon is always the brand's green.
|
|
3
|
+
import { MARK_SHAPES } from "./mark.js";
|
|
4
|
+
export { MARK_SHAPES };
|
|
5
|
+
function escape(text) {
|
|
6
|
+
return text.replace(/[&<>"']/g, (char) => `&#${char.charCodeAt(0)};`);
|
|
7
|
+
}
|
|
8
|
+
/** The mark as an `<svg>`: decorative by default; with `title`, an image with that name for screen readers. */
|
|
9
|
+
export function markSvg(options = {}) {
|
|
10
|
+
const size = options.size ?? 24;
|
|
11
|
+
const label = options.title ? ` role="img" aria-label="${escape(options.title)}"` : ' aria-hidden="true" focusable="false"';
|
|
12
|
+
const cls = options.className ? ` class="${escape(options.className)}"` : "";
|
|
13
|
+
return `<svg${cls} viewBox="0 0 100 100" width="${escape(String(size))}" height="${escape(String(size))}"${label}>${options.title ? `<title>${escape(options.title)}</title>` : ""}${MARK_SHAPES}</svg>`;
|
|
14
|
+
}
|
package/dist/mark.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
+
export declare const MARK_SHAPES = "<path d=\"M14 50 Q50 14 86 50 Q50 86 14 50Z\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"7\" stroke-linejoin=\"round\"/><path d=\"M51.1 40.2 A11 11 0 1 0 59.77 53.22 A7.86 7.86 0 0 1 51.1 40.2Z\" fill=\"#3fbf6f\"/><path d=\"M20.9258 28C20.6615 27.82 20.4223 27.6072 20.2143 27.3671C19.9961 26.9641 19.8024 26.549 19.6343 26.1238C17.3476 21.0763 13.2746 16.9981 8.15841 14.6333C7.94143 14.5501 7.7297 14.4545 7.5243 14.347C6.80512 13.9024 6.82832 13.2921 7.57843 12.84C7.74361 12.7589 7.91411 12.6884 8.08881 12.6291C13.3854 10.5226 17.6112 6.45934 19.8431 1.32691C20.0751 0.814541 20.0906 0.0309247 20.8562 0.000785591C21.6217 -0.0293535 21.7377 0.814541 21.9852 1.35704C23.0822 3.90828 24.6892 6.22141 26.7125 8.16176C28.7358 10.1021 31.135 11.6309 33.7704 12.6592C33.9933 12.7315 34.2104 12.8196 34.42 12.9229C35.1933 13.3524 35.1933 13.925 34.42 14.3621C33.9406 14.6333 33.3992 14.8066 32.8734 15.0628C28.0444 17.4681 24.2329 21.4457 22.1012 26.3047C21.9511 26.6872 21.7755 27.0597 21.5753 27.4198C21.3859 27.6401 21.1675 27.8352 20.9258 28Z\" fill=\"currentColor\" transform=\"translate(57.64 44.71) scale(0.3367) translate(-21 -14)\"/>";
|
|
1
2
|
export declare const MARK = "<svg class=\"mark\" part=\"mark\" viewBox=\"0 0 100 100\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M14 50 Q50 14 86 50 Q50 86 14 50Z\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"7\" stroke-linejoin=\"round\"/><path d=\"M51.1 40.2 A11 11 0 1 0 59.77 53.22 A7.86 7.86 0 0 1 51.1 40.2Z\" fill=\"#3fbf6f\"/><path d=\"M20.9258 28C20.6615 27.82 20.4223 27.6072 20.2143 27.3671C19.9961 26.9641 19.8024 26.549 19.6343 26.1238C17.3476 21.0763 13.2746 16.9981 8.15841 14.6333C7.94143 14.5501 7.7297 14.4545 7.5243 14.347C6.80512 13.9024 6.82832 13.2921 7.57843 12.84C7.74361 12.7589 7.91411 12.6884 8.08881 12.6291C13.3854 10.5226 17.6112 6.45934 19.8431 1.32691C20.0751 0.814541 20.0906 0.0309247 20.8562 0.000785591C21.6217 -0.0293535 21.7377 0.814541 21.9852 1.35704C23.0822 3.90828 24.6892 6.22141 26.7125 8.16176C28.7358 10.1021 31.135 11.6309 33.7704 12.6592C33.9933 12.7315 34.2104 12.8196 34.42 12.9229C35.1933 13.3524 35.1933 13.925 34.42 14.3621C33.9406 14.6333 33.3992 14.8066 32.8734 15.0628C28.0444 17.4681 24.2329 21.4457 22.1012 26.3047C21.9511 26.6872 21.7755 27.0597 21.5753 27.4198C21.3859 27.6401 21.1675 27.8352 20.9258 28Z\" fill=\"currentColor\" transform=\"translate(57.64 44.71) scale(0.3367) translate(-21 -14)\"/></svg>";
|
package/dist/mark.js
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
// SENTINEL's mark (generated from the main repo's src/brand.ts by `pnpm brand`; do not edit by hand).
|
|
1
|
+
// SENTINEL's mark (generated from the main repo's src/brand.ts by `pnpm brand`; do not edit by hand). The eye and
|
|
2
|
+
// the star take the text colour (currentColor); the moon is always the brand's green.
|
|
3
|
+
export const MARK_SHAPES = "<path d=\"M14 50 Q50 14 86 50 Q50 86 14 50Z\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"7\" stroke-linejoin=\"round\"/><path d=\"M51.1 40.2 A11 11 0 1 0 59.77 53.22 A7.86 7.86 0 0 1 51.1 40.2Z\" fill=\"#3fbf6f\"/><path d=\"M20.9258 28C20.6615 27.82 20.4223 27.6072 20.2143 27.3671C19.9961 26.9641 19.8024 26.549 19.6343 26.1238C17.3476 21.0763 13.2746 16.9981 8.15841 14.6333C7.94143 14.5501 7.7297 14.4545 7.5243 14.347C6.80512 13.9024 6.82832 13.2921 7.57843 12.84C7.74361 12.7589 7.91411 12.6884 8.08881 12.6291C13.3854 10.5226 17.6112 6.45934 19.8431 1.32691C20.0751 0.814541 20.0906 0.0309247 20.8562 0.000785591C21.6217 -0.0293535 21.7377 0.814541 21.9852 1.35704C23.0822 3.90828 24.6892 6.22141 26.7125 8.16176C28.7358 10.1021 31.135 11.6309 33.7704 12.6592C33.9933 12.7315 34.2104 12.8196 34.42 12.9229C35.1933 13.3524 35.1933 13.925 34.42 14.3621C33.9406 14.6333 33.3992 14.8066 32.8734 15.0628C28.0444 17.4681 24.2329 21.4457 22.1012 26.3047C21.9511 26.6872 21.7755 27.0597 21.5753 27.4198C21.3859 27.6401 21.1675 27.8352 20.9258 28Z\" fill=\"currentColor\" transform=\"translate(57.64 44.71) scale(0.3367) translate(-21 -14)\"/>";
|
|
2
4
|
export const MARK = "<svg class=\"mark\" part=\"mark\" viewBox=\"0 0 100 100\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M14 50 Q50 14 86 50 Q50 86 14 50Z\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"7\" stroke-linejoin=\"round\"/><path d=\"M51.1 40.2 A11 11 0 1 0 59.77 53.22 A7.86 7.86 0 0 1 51.1 40.2Z\" fill=\"#3fbf6f\"/><path d=\"M20.9258 28C20.6615 27.82 20.4223 27.6072 20.2143 27.3671C19.9961 26.9641 19.8024 26.549 19.6343 26.1238C17.3476 21.0763 13.2746 16.9981 8.15841 14.6333C7.94143 14.5501 7.7297 14.4545 7.5243 14.347C6.80512 13.9024 6.82832 13.2921 7.57843 12.84C7.74361 12.7589 7.91411 12.6884 8.08881 12.6291C13.3854 10.5226 17.6112 6.45934 19.8431 1.32691C20.0751 0.814541 20.0906 0.0309247 20.8562 0.000785591C21.6217 -0.0293535 21.7377 0.814541 21.9852 1.35704C23.0822 3.90828 24.6892 6.22141 26.7125 8.16176C28.7358 10.1021 31.135 11.6309 33.7704 12.6592C33.9933 12.7315 34.2104 12.8196 34.42 12.9229C35.1933 13.3524 35.1933 13.925 34.42 14.3621C33.9406 14.6333 33.3992 14.8066 32.8734 15.0628C28.0444 17.4681 24.2329 21.4457 22.1012 26.3047C21.9511 26.6872 21.7755 27.0597 21.5753 27.4198C21.3859 27.6401 21.1675 27.8352 20.9258 28Z\" fill=\"currentColor\" transform=\"translate(57.64 44.71) scale(0.3367) translate(-21 -14)\"/></svg>";
|
package/dist/react.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
-
import { type Report } from "./core.js";
|
|
2
|
+
import { type Privacy, type Report } from "./core.js";
|
|
3
3
|
import type { Texts } from "./element.js";
|
|
4
4
|
import "./element.js";
|
|
5
5
|
export type ReportButtonProps = {
|
|
@@ -20,12 +20,51 @@ export type ReportButtonProps = {
|
|
|
20
20
|
children?: ReactNode;
|
|
21
21
|
};
|
|
22
22
|
export declare function ReportButton({ site, endpoint, label, heading, position, theme, hideMark, texts, className, style, onSent, onError, children }: ReportButtonProps): import("react").DOMElement<Record<string, unknown>, Element>;
|
|
23
|
+
export type SentinelMarkProps = {
|
|
24
|
+
/** Width and height (a number is px; default `1em`, the size of the text around it). */
|
|
25
|
+
size?: number | string;
|
|
26
|
+
/** A name for screen readers; without it, the mark is decorative. */
|
|
27
|
+
title?: string;
|
|
28
|
+
className?: string;
|
|
29
|
+
style?: CSSProperties;
|
|
30
|
+
};
|
|
31
|
+
/** SENTINEL's mark, for your own button or footer. It takes the text colour (`color`); the moon stays green. */
|
|
32
|
+
export declare function SentinelMark({ size, title, className, style }: SentinelMarkProps): import("react").DOMElement<{
|
|
33
|
+
dangerouslySetInnerHTML: {
|
|
34
|
+
__html: string;
|
|
35
|
+
};
|
|
36
|
+
role: string;
|
|
37
|
+
"aria-label": string;
|
|
38
|
+
"aria-hidden"?: undefined;
|
|
39
|
+
focusable?: undefined;
|
|
40
|
+
viewBox: string;
|
|
41
|
+
width: string | number;
|
|
42
|
+
height: string | number;
|
|
43
|
+
className: string | undefined;
|
|
44
|
+
style: CSSProperties | undefined;
|
|
45
|
+
} | {
|
|
46
|
+
dangerouslySetInnerHTML: {
|
|
47
|
+
__html: string;
|
|
48
|
+
};
|
|
49
|
+
"aria-hidden": boolean;
|
|
50
|
+
focusable: string;
|
|
51
|
+
role?: undefined;
|
|
52
|
+
"aria-label"?: undefined;
|
|
53
|
+
viewBox: string;
|
|
54
|
+
width: string | number;
|
|
55
|
+
height: string | number;
|
|
56
|
+
className: string | undefined;
|
|
57
|
+
style: CSSProperties | undefined;
|
|
58
|
+
}, Element>;
|
|
23
59
|
export type ReportStatus = "idle" | "sending" | "sent" | "error";
|
|
24
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* For your own form: records the visit from the first render and sends with its context. `route`, `redact` and
|
|
62
|
+
* `title` clean what is sent (see `Privacy`); pass stable functions (defined outside the component).
|
|
63
|
+
*/
|
|
25
64
|
export declare function useSentinelReport(options: {
|
|
26
65
|
site: string;
|
|
27
66
|
endpoint?: string;
|
|
28
|
-
}): {
|
|
67
|
+
} & Privacy): {
|
|
29
68
|
send: (report: Report) => Promise<boolean>;
|
|
30
69
|
status: ReportStatus;
|
|
31
70
|
error: unknown;
|
package/dist/react.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// own form (`useSentinelReport`, which records the context and sends; the look is all yours).
|
|
3
3
|
import { createElement, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { capture, createReporter } from "./core.js";
|
|
5
|
+
import { MARK_SHAPES } from "./mark.js";
|
|
5
6
|
import "./element.js";
|
|
6
7
|
export function ReportButton({ site, endpoint, label, heading, position, theme, hideMark, texts, className, style, onSent, onError, children }) {
|
|
7
8
|
const ref = useRef(null);
|
|
@@ -25,9 +26,17 @@ export function ReportButton({ site, endpoint, label, heading, position, theme,
|
|
|
25
26
|
attributes["hide-mark"] = "";
|
|
26
27
|
return createElement("sentinel-report", attributes, children === undefined ? undefined : createElement("span", { slot: "trigger", style: { display: "contents" } }, children));
|
|
27
28
|
}
|
|
28
|
-
/**
|
|
29
|
+
/** SENTINEL's mark, for your own button or footer. It takes the text colour (`color`); the moon stays green. */
|
|
30
|
+
export function SentinelMark({ size = "1em", title, className, style }) {
|
|
31
|
+
const label = title ? { role: "img", "aria-label": title } : { "aria-hidden": true, focusable: "false" };
|
|
32
|
+
return createElement("svg", { viewBox: "0 0 100 100", width: size, height: size, className, style, ...label, dangerouslySetInnerHTML: { __html: `${title ? `<title>${title.replace(/[&<>"']/g, (char) => `&#${char.charCodeAt(0)};`)}</title>` : ""}${MARK_SHAPES}` } });
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* For your own form: records the visit from the first render and sends with its context. `route`, `redact` and
|
|
36
|
+
* `title` clean what is sent (see `Privacy`); pass stable functions (defined outside the component).
|
|
37
|
+
*/
|
|
29
38
|
export function useSentinelReport(options) {
|
|
30
|
-
const reporter = useMemo(() => (typeof window === "undefined" ? undefined : createReporter(options)), [options.site, options.endpoint]);
|
|
39
|
+
const reporter = useMemo(() => (typeof window === "undefined" ? undefined : createReporter(options)), [options.site, options.endpoint, options.route, options.redact, options.title]);
|
|
31
40
|
const [status, setStatus] = useState("idle");
|
|
32
41
|
const [error, setError] = useState(undefined);
|
|
33
42
|
useEffect(() => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@universelle/sentinel-report",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "SENTINEL's report button: let real users tell you what fails or what they miss, with the technical context of their visit.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"files": [
|
|
27
27
|
"dist",
|
|
28
28
|
"README.md",
|
|
29
|
+
"CHANGELOG.md",
|
|
29
30
|
"LICENSE"
|
|
30
31
|
],
|
|
31
32
|
"scripts": {
|