@blario/react-native 0.0.0 → 0.1.1
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 +21 -0
- package/README.md +401 -0
- package/dist/BlarProvider.d.ts +74 -0
- package/dist/actions/batched.d.ts +1 -0
- package/dist/actions/click.d.ts +4 -0
- package/dist/actions/commit-watch.d.ts +15 -0
- package/dist/actions/events.d.ts +35 -0
- package/dist/actions/executor.d.ts +29 -0
- package/dist/actions/highlight.d.ts +4 -0
- package/dist/actions/interaction-guards.d.ts +9 -0
- package/dist/actions/landing.d.ts +11 -0
- package/dist/actions/navigation-driver.d.ts +22 -0
- package/dist/actions/parse.d.ts +4 -0
- package/dist/actions/result.d.ts +4 -0
- package/dist/actions/scrolling.d.ts +12 -0
- package/dist/actions/services.d.ts +20 -0
- package/dist/actions/settle.d.ts +13 -0
- package/dist/actions/switch.d.ts +3 -0
- package/dist/actions/target.d.ts +25 -0
- package/dist/actions/text-input.d.ts +9 -0
- package/dist/actions/types.d.ts +59 -0
- package/dist/agent/batch-commands.d.ts +17 -0
- package/dist/agent/compose.d.ts +23 -0
- package/dist/agent/controller.d.ts +28 -0
- package/dist/agent/frame-runner.d.ts +9 -0
- package/dist/agent/frames.d.ts +180 -0
- package/dist/agent/handlers/agent-command.d.ts +5 -0
- package/dist/agent/handlers/batch.d.ts +5 -0
- package/dist/agent/handlers/consent-screenshot.d.ts +4 -0
- package/dist/agent/handlers/context.d.ts +16 -0
- package/dist/agent/handlers/dom-state.d.ts +4 -0
- package/dist/agent/handlers/element-context.d.ts +2 -0
- package/dist/agent/handlers/screenshot.d.ts +3 -0
- package/dist/agent/handlers/text-dom.d.ts +5 -0
- package/dist/agent/stage.d.ts +13 -0
- package/dist/agent-mode/action-description.d.ts +3 -0
- package/dist/agent-mode/bubbles.d.ts +10 -0
- package/dist/agent-mode/colors.d.ts +12 -0
- package/dist/agent-mode/controller.d.ts +92 -0
- package/dist/agent-mode/cursor-follow.d.ts +12 -0
- package/dist/agent-mode/cursor.d.ts +37 -0
- package/dist/agent-mode/frames.d.ts +36 -0
- package/dist/agent-mode/haptics.d.ts +6 -0
- package/dist/agent-mode/keep-awake.d.ts +19 -0
- package/dist/agent-mode/layer-stacking.d.ts +7 -0
- package/dist/agent-mode/markdown.d.ts +19 -0
- package/dist/agent-mode/panel-geometry.d.ts +6 -0
- package/dist/agent-mode/resume.d.ts +13 -0
- package/dist/agent-mode/run-memory.d.ts +19 -0
- package/dist/agent-mode/spotlight-controller.d.ts +68 -0
- package/dist/agent-mode/spotlight.d.ts +6 -0
- package/dist/agent-mode/state.d.ts +9 -0
- package/dist/agent-mode/status.d.ts +11 -0
- package/dist/agent-mode/timers.d.ts +6 -0
- package/dist/agent-mode/ui/AgentDock.d.ts +37 -0
- package/dist/agent-mode/ui/AgentLayer.d.ts +19 -0
- package/dist/agent-mode/ui/AgentLayerHost.d.ts +8 -0
- package/dist/agent-mode/ui/AgentPanel.d.ts +19 -0
- package/dist/agent-mode/ui/AgentTap.d.ts +10 -0
- package/dist/agent-mode/ui/MarkdownText.d.ts +8 -0
- package/dist/agent-mode/ui/MessageBubbles.d.ts +9 -0
- package/dist/agent-mode/ui/PendingOverlay.d.ts +5 -0
- package/dist/agent-mode/ui/SpotlightLayer.d.ts +9 -0
- package/dist/agent-mode/ui/SpotlightOverlay.d.ts +7 -0
- package/dist/agent-mode/ui/StatusCard.d.ts +14 -0
- package/dist/agent-mode/ui/TargetPulse.d.ts +9 -0
- package/dist/agent-mode/ui/TouchShield.d.ts +5 -0
- package/dist/agent-mode/ui/WarningIcon.d.ts +6 -0
- package/dist/agent-mode/ui/full-window-overlay.d.ts +5 -0
- package/dist/agent-mode/ui/layer-host.d.ts +2 -0
- package/dist/backend/api-client.d.ts +38 -0
- package/dist/backend/api-types.d.ts +25 -0
- package/dist/backend/app-info.d.ts +14 -0
- package/dist/backend/backend-transport.d.ts +70 -0
- package/dist/backend/backoff.d.ts +9 -0
- package/dist/backend/base64.d.ts +1 -0
- package/dist/backend/errors.d.ts +17 -0
- package/dist/backend/foreground.d.ts +1 -0
- package/dist/backend/health.d.ts +17 -0
- package/dist/backend/identity-proof.d.ts +23 -0
- package/dist/backend/identity.d.ts +27 -0
- package/dist/backend/message-metadata.d.ts +19 -0
- package/dist/backend/presence.d.ts +20 -0
- package/dist/backend/response-cache.d.ts +9 -0
- package/dist/backend/session-change.d.ts +2 -0
- package/dist/backend/session-socket.d.ts +49 -0
- package/dist/backend/session.d.ts +39 -0
- package/dist/backend/socket-url.d.ts +9 -0
- package/dist/backend/storage.d.ts +13 -0
- package/dist/backend/tab-id.d.ts +2 -0
- package/dist/blar.d.ts +37 -0
- package/dist/chat/ChatLayer.d.ts +29 -0
- package/dist/chat/ChatSheet.d.ts +11 -0
- package/dist/chat/ChatView.d.ts +19 -0
- package/dist/chat/Launcher.d.ts +14 -0
- package/dist/chat/bridge.d.ts +22 -0
- package/dist/chat/chat-coordinator.d.ts +57 -0
- package/dist/chat/chat-state.d.ts +20 -0
- package/dist/chat/navigation.d.ts +5 -0
- package/dist/chat/options.d.ts +22 -0
- package/dist/chat/webview.d.ts +10 -0
- package/dist/chat/widget-bundle.generated.d.ts +2 -0
- package/dist/chat/widget-shell.d.ts +24 -0
- package/dist/confirmation/port.d.ts +12 -0
- package/dist/fiber/locate.d.ts +7 -0
- package/dist/fiber/narrow.d.ts +5 -0
- package/dist/fiber/reader.d.ts +10 -0
- package/dist/fiber/text.d.ts +4 -0
- package/dist/fiber/types.d.ts +21 -0
- package/dist/host/border-radius.d.ts +1 -0
- package/dist/host/instance.d.ts +9 -0
- package/dist/host/measure.d.ts +3 -0
- package/dist/host/rect.d.ts +22 -0
- package/dist/host/visible-area.d.ts +2 -0
- package/dist/index.d.ts +10 -0
- package/dist/nudge/NudgeBubble.d.ts +15 -0
- package/dist/nudge/nudge.d.ts +23 -0
- package/dist/optional/expo-haptics.d.ts +1 -0
- package/dist/optional/expo-keep-awake.d.ts +1 -0
- package/dist/optional/react-native-haptic-feedback.d.ts +1 -0
- package/dist/optional/react-native-keep-awake.d.ts +1 -0
- package/dist/overlay/AgentOverlay.d.ts +5 -0
- package/dist/screenshot/capture-backend.d.ts +6 -0
- package/dist/screenshot/screenshotter.d.ts +18 -0
- package/dist/screenshot/skia.d.ts +35 -0
- package/dist/screenshot/view-shot.d.ts +2 -0
- package/dist/snapshot/capture.d.ts +36 -0
- package/dist/snapshot/classify.d.ts +11 -0
- package/dist/snapshot/collect.d.ts +27 -0
- package/dist/snapshot/element-context.d.ts +9 -0
- package/dist/snapshot/icon.d.ts +2 -0
- package/dist/snapshot/kinds.d.ts +3 -0
- package/dist/snapshot/modals.d.ts +2 -0
- package/dist/snapshot/navigation.d.ts +14 -0
- package/dist/snapshot/ordering.d.ts +2 -0
- package/dist/snapshot/properties.d.ts +14 -0
- package/dist/snapshot/registry.d.ts +16 -0
- package/dist/snapshot/render.d.ts +10 -0
- package/dist/snapshot/route-summary.d.ts +3 -0
- package/dist/snapshot/scroll.d.ts +15 -0
- package/dist/snapshot/text-layout.d.ts +8 -0
- package/dist/snapshot/view.d.ts +30 -0
- package/dist/snapshot/visibility.d.ts +17 -0
- package/dist/snapshot/wrappers.d.ts +11 -0
- package/dist/telemetry/SdkErrorBoundary.d.ts +15 -0
- package/dist/telemetry/build-info.generated.d.ts +2 -0
- package/dist/telemetry/envelope.d.ts +53 -0
- package/dist/telemetry/reporter.d.ts +33 -0
- package/dist/telemetry/scrub.d.ts +1 -0
- package/dist/telemetry/sdk-reporter.d.ts +5 -0
- package/dist/transport/connection-options.d.ts +18 -0
- package/dist/transport/relay-transport.d.ts +17 -0
- package/dist/transport/transport.d.ts +7 -0
- package/dist/ui/insets.d.ts +8 -0
- package/dist/ui/strings.d.ts +33 -0
- package/dist/ui/useInsets.d.ts +2 -0
- package/dist/version.d.ts +1 -0
- package/package.json +87 -3
- package/src/BlarProvider.tsx +462 -0
- package/src/actions/batched.ts +16 -0
- package/src/actions/click.ts +54 -0
- package/src/actions/commit-watch.ts +86 -0
- package/src/actions/events.ts +58 -0
- package/src/actions/executor.ts +147 -0
- package/src/actions/highlight.ts +13 -0
- package/src/actions/interaction-guards.ts +44 -0
- package/src/actions/landing.ts +26 -0
- package/src/actions/navigation-driver.ts +131 -0
- package/src/actions/parse.ts +82 -0
- package/src/actions/result.ts +14 -0
- package/src/actions/scrolling.ts +112 -0
- package/src/actions/services.ts +24 -0
- package/src/actions/settle.ts +40 -0
- package/src/actions/switch.ts +31 -0
- package/src/actions/target.ts +77 -0
- package/src/actions/text-input.ts +82 -0
- package/src/actions/types.ts +52 -0
- package/src/agent/batch-commands.ts +33 -0
- package/src/agent/compose.ts +67 -0
- package/src/agent/controller.ts +106 -0
- package/src/agent/frame-runner.ts +27 -0
- package/src/agent/frames.ts +193 -0
- package/src/agent/handlers/agent-command.ts +74 -0
- package/src/agent/handlers/batch.ts +72 -0
- package/src/agent/handlers/consent-screenshot.ts +44 -0
- package/src/agent/handlers/context.ts +17 -0
- package/src/agent/handlers/dom-state.ts +19 -0
- package/src/agent/handlers/element-context.ts +9 -0
- package/src/agent/handlers/screenshot.ts +24 -0
- package/src/agent/handlers/text-dom.ts +17 -0
- package/src/agent/stage.ts +22 -0
- package/src/agent-mode/action-description.ts +25 -0
- package/src/agent-mode/bubbles.ts +31 -0
- package/src/agent-mode/colors.ts +40 -0
- package/src/agent-mode/controller.ts +459 -0
- package/src/agent-mode/cursor-follow.ts +43 -0
- package/src/agent-mode/cursor.ts +118 -0
- package/src/agent-mode/frames.ts +70 -0
- package/src/agent-mode/haptics.ts +49 -0
- package/src/agent-mode/keep-awake.ts +107 -0
- package/src/agent-mode/layer-stacking.ts +16 -0
- package/src/agent-mode/markdown.ts +84 -0
- package/src/agent-mode/panel-geometry.ts +27 -0
- package/src/agent-mode/resume.ts +16 -0
- package/src/agent-mode/run-memory.ts +65 -0
- package/src/agent-mode/spotlight-controller.ts +255 -0
- package/src/agent-mode/spotlight.ts +14 -0
- package/src/agent-mode/state.ts +36 -0
- package/src/agent-mode/status.ts +13 -0
- package/src/agent-mode/timers.ts +11 -0
- package/src/agent-mode/ui/AgentDock.tsx +285 -0
- package/src/agent-mode/ui/AgentLayer.tsx +123 -0
- package/src/agent-mode/ui/AgentLayerHost.tsx +37 -0
- package/src/agent-mode/ui/AgentPanel.tsx +187 -0
- package/src/agent-mode/ui/AgentTap.tsx +190 -0
- package/src/agent-mode/ui/MarkdownText.tsx +55 -0
- package/src/agent-mode/ui/MessageBubbles.tsx +58 -0
- package/src/agent-mode/ui/PendingOverlay.tsx +32 -0
- package/src/agent-mode/ui/SpotlightLayer.tsx +60 -0
- package/src/agent-mode/ui/SpotlightOverlay.tsx +35 -0
- package/src/agent-mode/ui/StatusCard.tsx +137 -0
- package/src/agent-mode/ui/TargetPulse.tsx +74 -0
- package/src/agent-mode/ui/TouchShield.tsx +22 -0
- package/src/agent-mode/ui/WarningIcon.tsx +32 -0
- package/src/agent-mode/ui/full-window-overlay.ts +16 -0
- package/src/agent-mode/ui/layer-host.ts +11 -0
- package/src/backend/api-client.ts +210 -0
- package/src/backend/api-types.ts +56 -0
- package/src/backend/app-info.ts +40 -0
- package/src/backend/backend-transport.ts +338 -0
- package/src/backend/backoff.ts +16 -0
- package/src/backend/base64.ts +25 -0
- package/src/backend/errors.ts +32 -0
- package/src/backend/foreground.ts +10 -0
- package/src/backend/health.ts +64 -0
- package/src/backend/identity-proof.ts +151 -0
- package/src/backend/identity.ts +31 -0
- package/src/backend/message-metadata.ts +28 -0
- package/src/backend/presence.ts +95 -0
- package/src/backend/response-cache.ts +45 -0
- package/src/backend/session-change.ts +14 -0
- package/src/backend/session-socket.ts +292 -0
- package/src/backend/session.ts +188 -0
- package/src/backend/socket-url.ts +25 -0
- package/src/backend/storage.ts +49 -0
- package/src/backend/tab-id.ts +24 -0
- package/src/blar.ts +100 -0
- package/src/chat/ChatLayer.tsx +109 -0
- package/src/chat/ChatSheet.tsx +84 -0
- package/src/chat/ChatView.tsx +133 -0
- package/src/chat/Launcher.tsx +72 -0
- package/src/chat/bridge.ts +56 -0
- package/src/chat/chat-coordinator.ts +189 -0
- package/src/chat/chat-state.ts +35 -0
- package/src/chat/navigation.ts +15 -0
- package/src/chat/options.ts +39 -0
- package/src/chat/webview.ts +23 -0
- package/src/chat/widget-bundle.generated.ts +2 -0
- package/src/chat/widget-shell.ts +170 -0
- package/src/confirmation/port.ts +15 -0
- package/src/fiber/locate.ts +41 -0
- package/src/fiber/narrow.ts +15 -0
- package/src/fiber/reader.ts +98 -0
- package/src/fiber/text.ts +52 -0
- package/src/fiber/types.ts +24 -0
- package/src/host/border-radius.ts +11 -0
- package/src/host/instance.ts +30 -0
- package/src/host/measure.ts +25 -0
- package/src/host/rect.ts +45 -0
- package/src/host/visible-area.ts +5 -0
- package/src/index.ts +10 -0
- package/src/nudge/NudgeBubble.tsx +117 -0
- package/src/nudge/nudge.ts +100 -0
- package/src/optional/expo-haptics.ts +11 -0
- package/src/optional/expo-keep-awake.ts +11 -0
- package/src/optional/react-native-haptic-feedback.ts +11 -0
- package/src/optional/react-native-keep-awake.ts +11 -0
- package/src/overlay/AgentOverlay.tsx +16 -0
- package/src/screenshot/capture-backend.ts +7 -0
- package/src/screenshot/screenshotter.ts +39 -0
- package/src/screenshot/skia.ts +107 -0
- package/src/screenshot/view-shot.ts +41 -0
- package/src/snapshot/capture.ts +163 -0
- package/src/snapshot/classify.ts +40 -0
- package/src/snapshot/collect.ts +136 -0
- package/src/snapshot/element-context.ts +66 -0
- package/src/snapshot/icon.ts +21 -0
- package/src/snapshot/kinds.ts +10 -0
- package/src/snapshot/modals.ts +21 -0
- package/src/snapshot/navigation.ts +47 -0
- package/src/snapshot/ordering.ts +26 -0
- package/src/snapshot/properties.ts +71 -0
- package/src/snapshot/registry.ts +33 -0
- package/src/snapshot/render.ts +140 -0
- package/src/snapshot/route-summary.ts +15 -0
- package/src/snapshot/scroll.ts +90 -0
- package/src/snapshot/text-layout.ts +59 -0
- package/src/snapshot/view.ts +87 -0
- package/src/snapshot/visibility.ts +63 -0
- package/src/snapshot/wrappers.ts +69 -0
- package/src/telemetry/SdkErrorBoundary.tsx +28 -0
- package/src/telemetry/build-info.generated.ts +2 -0
- package/src/telemetry/envelope.ts +83 -0
- package/src/telemetry/reporter.ts +170 -0
- package/src/telemetry/scrub.ts +10 -0
- package/src/telemetry/sdk-reporter.ts +37 -0
- package/src/transport/connection-options.ts +65 -0
- package/src/transport/relay-transport.ts +82 -0
- package/src/transport/transport.ts +8 -0
- package/src/ui/insets.ts +31 -0
- package/src/ui/strings.ts +102 -0
- package/src/ui/useInsets.ts +10 -0
- package/src/version.ts +1 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Rect } from '../host/rect.ts';
|
|
2
|
+
|
|
3
|
+
export interface PositionedText {
|
|
4
|
+
rect: Rect;
|
|
5
|
+
text: string;
|
|
6
|
+
heading: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const SAME_LINE_OVERLAP = 0.5;
|
|
10
|
+
const LINE_SEPARATOR = ' ';
|
|
11
|
+
|
|
12
|
+
function verticalOverlap(first: Rect, second: Rect): number {
|
|
13
|
+
const top = Math.max(first.y, second.y);
|
|
14
|
+
const bottom = Math.min(first.y + first.height, second.y + second.height);
|
|
15
|
+
return Math.max(0, bottom - top);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function sharesLine(line: PositionedText[], candidate: PositionedText): boolean {
|
|
19
|
+
const reference = line[line.length - 1];
|
|
20
|
+
if (reference === undefined || reference.heading || candidate.heading) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const shorter = Math.min(reference.rect.height, candidate.rect.height);
|
|
24
|
+
return shorter > 0 && verticalOverlap(reference.rect, candidate.rect) / shorter >= SAME_LINE_OVERLAP;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function groupIntoLines(blocks: PositionedText[]): string[] {
|
|
28
|
+
const sorted = [...blocks].sort((left, right) => left.rect.y - right.rect.y || left.rect.x - right.rect.x);
|
|
29
|
+
const lines: PositionedText[][] = [];
|
|
30
|
+
for (const block of sorted) {
|
|
31
|
+
const current = lines[lines.length - 1];
|
|
32
|
+
if (current !== undefined && sharesLine(current, block)) {
|
|
33
|
+
current.push(block);
|
|
34
|
+
} else {
|
|
35
|
+
lines.push([block]);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return lines.map((line) => {
|
|
39
|
+
const joined = [...line].sort((left, right) => left.rect.x - right.rect.x).map((block) => block.text).join(LINE_SEPARATOR);
|
|
40
|
+
return line.length === 1 && line[0]?.heading === true ? `# ${joined}` : joined;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function limitCharacters(lines: string[], maxChars: number): string[] {
|
|
45
|
+
if (maxChars <= 0) {
|
|
46
|
+
return lines;
|
|
47
|
+
}
|
|
48
|
+
const kept: string[] = [];
|
|
49
|
+
let used = 0;
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
if (used + line.length > maxChars) {
|
|
52
|
+
kept.push('…');
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
kept.push(line);
|
|
56
|
+
used += line.length + 1;
|
|
57
|
+
}
|
|
58
|
+
return kept;
|
|
59
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { collectText, precedingText } from '../fiber/text.ts';
|
|
2
|
+
import { propsOf } from '../fiber/reader.ts';
|
|
3
|
+
import type { Fiber } from '../fiber/types.ts';
|
|
4
|
+
import type { Rect } from '../host/rect.ts';
|
|
5
|
+
import { iconNameIn } from './icon.ts';
|
|
6
|
+
import type { Offscreen } from './visibility.ts';
|
|
7
|
+
import type { ElementKind } from './kinds.ts';
|
|
8
|
+
import {
|
|
9
|
+
accessibilityFlagsOf,
|
|
10
|
+
explicitLabelOf,
|
|
11
|
+
inputTypeOf,
|
|
12
|
+
placeholderOf,
|
|
13
|
+
roleOf,
|
|
14
|
+
testIdOf,
|
|
15
|
+
textValueOf,
|
|
16
|
+
} from './properties.ts';
|
|
17
|
+
|
|
18
|
+
const BUTTON_LABEL_LIMIT = 80;
|
|
19
|
+
const INPUT_LABEL_LIMIT = 40;
|
|
20
|
+
|
|
21
|
+
export interface ElementView {
|
|
22
|
+
kind: ElementKind;
|
|
23
|
+
label: string;
|
|
24
|
+
icon: string | null;
|
|
25
|
+
testID: string | null;
|
|
26
|
+
role: string | null;
|
|
27
|
+
type: string | null;
|
|
28
|
+
placeholder: string | null;
|
|
29
|
+
value: string | null;
|
|
30
|
+
checked: boolean | null;
|
|
31
|
+
disabled: boolean;
|
|
32
|
+
expanded: boolean | null;
|
|
33
|
+
selected: boolean | null;
|
|
34
|
+
scroll: string | null;
|
|
35
|
+
offscreen: Offscreen | null;
|
|
36
|
+
rect: Rect;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function labelFor(kind: ElementKind, fiber: Fiber, ancestors: Fiber[]): string {
|
|
40
|
+
const explicit = explicitLabelOf(propsOf(fiber));
|
|
41
|
+
if (explicit !== null) {
|
|
42
|
+
return explicit;
|
|
43
|
+
}
|
|
44
|
+
if (kind === 'button') {
|
|
45
|
+
return collectText(fiber, BUTTON_LABEL_LIMIT);
|
|
46
|
+
}
|
|
47
|
+
if (kind === 'text_input' || kind === 'switch') {
|
|
48
|
+
return precedingText(ancestors, fiber, INPUT_LABEL_LIMIT);
|
|
49
|
+
}
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ViewSource {
|
|
54
|
+
kind: ElementKind;
|
|
55
|
+
fiber: Fiber;
|
|
56
|
+
ancestors: Fiber[];
|
|
57
|
+
rect: Rect;
|
|
58
|
+
scroll: string | null;
|
|
59
|
+
offscreen?: Offscreen | null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function buildView(source: ViewSource): ElementView {
|
|
63
|
+
const { kind, fiber, ancestors, rect, scroll } = source;
|
|
64
|
+
const offscreen = source.offscreen ?? null;
|
|
65
|
+
const props = propsOf(fiber);
|
|
66
|
+
const flags = accessibilityFlagsOf(props);
|
|
67
|
+
const isInput = kind === 'text_input';
|
|
68
|
+
const isSwitch = kind === 'switch';
|
|
69
|
+
const role = roleOf(props);
|
|
70
|
+
return {
|
|
71
|
+
kind,
|
|
72
|
+
label: labelFor(kind, fiber, ancestors),
|
|
73
|
+
icon: kind === 'button' ? iconNameIn(fiber) : null,
|
|
74
|
+
testID: testIdOf(props),
|
|
75
|
+
role: kind === 'button' && role === 'button' ? null : role,
|
|
76
|
+
type: isInput ? inputTypeOf(props) : isSwitch ? 'checkbox' : null,
|
|
77
|
+
placeholder: isInput ? placeholderOf(props) : null,
|
|
78
|
+
value: isInput ? textValueOf(props) : null,
|
|
79
|
+
checked: isSwitch ? props.value === true : null,
|
|
80
|
+
disabled: isInput ? flags.disabled || props.editable === false : flags.disabled,
|
|
81
|
+
expanded: flags.expanded,
|
|
82
|
+
selected: flags.selected,
|
|
83
|
+
scroll,
|
|
84
|
+
offscreen,
|
|
85
|
+
rect,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Fiber } from '../fiber/types.ts';
|
|
2
|
+
import { intersects, isVisibleIn, type Rect, type Size } from '../host/rect.ts';
|
|
3
|
+
import { measureScrollGeometry, type ScrollGeometry } from './scroll.ts';
|
|
4
|
+
|
|
5
|
+
export type ScrollGeometries = Map<Fiber, ScrollGeometry | null>;
|
|
6
|
+
|
|
7
|
+
export async function measureScrollGeometries(containers: Iterable<Fiber>): Promise<ScrollGeometries> {
|
|
8
|
+
const unique = [...new Set(containers)];
|
|
9
|
+
const entries = await Promise.all(
|
|
10
|
+
unique.map(async (container): Promise<[Fiber, ScrollGeometry | null]> => [container, await measureScrollGeometry(container)]),
|
|
11
|
+
);
|
|
12
|
+
return new Map(entries);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type Offscreen = 'above' | 'below' | 'left' | 'right';
|
|
16
|
+
|
|
17
|
+
export type Visibility =
|
|
18
|
+
| { kind: 'visible' }
|
|
19
|
+
| { kind: 'offscreen'; direction: Offscreen; distance: number }
|
|
20
|
+
| { kind: 'hidden' };
|
|
21
|
+
|
|
22
|
+
const VISIBLE: Visibility = { kind: 'visible' };
|
|
23
|
+
const HIDDEN: Visibility = { kind: 'hidden' };
|
|
24
|
+
|
|
25
|
+
function offscreenAgainst(rect: Rect, geometry: ScrollGeometry): Visibility {
|
|
26
|
+
const { viewport, horizontal } = geometry;
|
|
27
|
+
if (horizontal) {
|
|
28
|
+
return rect.x >= viewport.x + viewport.width
|
|
29
|
+
? { kind: 'offscreen', direction: 'right', distance: rect.x - (viewport.x + viewport.width) }
|
|
30
|
+
: { kind: 'offscreen', direction: 'left', distance: viewport.x - (rect.x + rect.width) };
|
|
31
|
+
}
|
|
32
|
+
return rect.y >= viewport.y + viewport.height
|
|
33
|
+
? { kind: 'offscreen', direction: 'below', distance: rect.y - (viewport.y + viewport.height) }
|
|
34
|
+
: { kind: 'offscreen', direction: 'above', distance: viewport.y - (rect.y + rect.height) };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function classifyVisibility(rect: Rect, window: Size, containers: Fiber[], geometries: ScrollGeometries): Visibility {
|
|
38
|
+
if (rect.width <= 0 || rect.height <= 0) {
|
|
39
|
+
return HIDDEN;
|
|
40
|
+
}
|
|
41
|
+
for (let index = containers.length - 1; index >= 0; index -= 1) {
|
|
42
|
+
const container = containers[index];
|
|
43
|
+
const geometry = container === undefined ? null : (geometries.get(container) ?? null);
|
|
44
|
+
if (geometry !== null && !intersects(rect, geometry.viewport)) {
|
|
45
|
+
return isVisibleIn(geometry.viewport, window) ? offscreenAgainst(rect, geometry) : HIDDEN;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return isVisibleIn(rect, window) ? VISIBLE : HIDDEN;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isBeyondWindowEdge(rect: Rect, window: Size): boolean {
|
|
52
|
+
return rect.x >= window.width || rect.x + rect.width <= 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function keepInTextTree(visibility: Visibility, rect: Rect, window: Size, viewportOnly: boolean): boolean {
|
|
56
|
+
if (visibility.kind === 'offscreen' && (visibility.direction === 'left' || visibility.direction === 'right')) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
if (visibility.kind === 'hidden' && isBeyondWindowEdge(rect, window)) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return !viewportOnly || visibility.kind === 'visible';
|
|
63
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { propsOf } from '../fiber/reader.ts';
|
|
2
|
+
import type { Rect } from '../host/rect.ts';
|
|
3
|
+
import type { Candidate } from './collect.ts';
|
|
4
|
+
import type { Offscreen } from './visibility.ts';
|
|
5
|
+
|
|
6
|
+
export interface MeasuredCandidate {
|
|
7
|
+
candidate: Candidate;
|
|
8
|
+
rect: Rect;
|
|
9
|
+
offscreen: Offscreen | null;
|
|
10
|
+
distance: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const RECT_TOLERANCE = 1;
|
|
14
|
+
|
|
15
|
+
function handlerOf(candidate: Candidate): unknown {
|
|
16
|
+
const props = propsOf(candidate.fiber);
|
|
17
|
+
switch (candidate.kind) {
|
|
18
|
+
case 'button':
|
|
19
|
+
return props.onPress;
|
|
20
|
+
case 'text_input':
|
|
21
|
+
return props.onChangeText ?? props.onChange;
|
|
22
|
+
case 'switch':
|
|
23
|
+
return props.onValueChange;
|
|
24
|
+
case 'scroll':
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isSameRect(first: Rect, second: Rect): boolean {
|
|
30
|
+
return (
|
|
31
|
+
Math.abs(first.x - second.x) <= RECT_TOLERANCE &&
|
|
32
|
+
Math.abs(first.y - second.y) <= RECT_TOLERANCE &&
|
|
33
|
+
Math.abs(first.width - second.width) <= RECT_TOLERANCE &&
|
|
34
|
+
Math.abs(first.height - second.height) <= RECT_TOLERANCE
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isWrapperOf(outer: MeasuredCandidate, inner: MeasuredCandidate): boolean {
|
|
39
|
+
if (outer.candidate.kind !== inner.candidate.kind || outer.candidate.kind === 'scroll') {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
const sharedHandler = handlerOf(outer.candidate) !== undefined && handlerOf(outer.candidate) === handlerOf(inner.candidate);
|
|
43
|
+
return sharedHandler || isSameRect(outer.rect, inner.rect);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function dropWrapperDuplicates(measured: MeasuredCandidate[]): MeasuredCandidate[] {
|
|
47
|
+
const byFiber = new Map<unknown, MeasuredCandidate>(measured.map((entry) => [entry.candidate.fiber, entry]));
|
|
48
|
+
const wrappers = new Set<MeasuredCandidate>();
|
|
49
|
+
for (const inner of measured) {
|
|
50
|
+
for (const ancestor of inner.candidate.ancestors) {
|
|
51
|
+
const outer = byFiber.get(ancestor);
|
|
52
|
+
if (outer !== undefined && isWrapperOf(outer, inner)) {
|
|
53
|
+
wrappers.add(outer);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return measured.filter((entry) => !wrappers.has(entry));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const OFFSCREEN_LIMIT = 30;
|
|
61
|
+
|
|
62
|
+
export function limitOffscreen(measured: MeasuredCandidate[]): MeasuredCandidate[] {
|
|
63
|
+
const hidden = measured.filter((entry) => entry.offscreen !== null && entry.candidate.kind !== 'scroll');
|
|
64
|
+
if (hidden.length <= OFFSCREEN_LIMIT) {
|
|
65
|
+
return measured;
|
|
66
|
+
}
|
|
67
|
+
const dropped = new Set(hidden.sort((left, right) => left.distance - right.distance).slice(OFFSCREEN_LIMIT));
|
|
68
|
+
return measured.filter((entry) => !dropped.has(entry));
|
|
69
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React, { Component, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { reportSdkError } from './sdk-reporter.ts';
|
|
4
|
+
|
|
5
|
+
interface SdkErrorBoundaryProps {
|
|
6
|
+
context: string;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface SdkErrorBoundaryState {
|
|
11
|
+
failed: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class SdkErrorBoundary extends Component<SdkErrorBoundaryProps, SdkErrorBoundaryState> {
|
|
15
|
+
override state: SdkErrorBoundaryState = { failed: false };
|
|
16
|
+
|
|
17
|
+
static getDerivedStateFromError(): SdkErrorBoundaryState {
|
|
18
|
+
return { failed: true };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
override componentDidCatch(error: Error): void {
|
|
22
|
+
reportSdkError(this.props.context, error);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
override render(): React.ReactNode {
|
|
26
|
+
return this.state.failed ? null : this.props.children;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export type StateSnapshot = Record<string, string | number | boolean>;
|
|
2
|
+
|
|
3
|
+
export interface StackFrame {
|
|
4
|
+
function?: string;
|
|
5
|
+
filename: string;
|
|
6
|
+
lineno: number;
|
|
7
|
+
colno: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface Breadcrumb {
|
|
11
|
+
timestamp: number;
|
|
12
|
+
category: string;
|
|
13
|
+
message: string;
|
|
14
|
+
level: 'info';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ExceptionValue {
|
|
18
|
+
type: string;
|
|
19
|
+
value: string;
|
|
20
|
+
stacktrace: { frames: StackFrame[] };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SentryEvent {
|
|
24
|
+
event_id: string;
|
|
25
|
+
timestamp: number;
|
|
26
|
+
platform: 'javascript';
|
|
27
|
+
level: 'error';
|
|
28
|
+
environment: string;
|
|
29
|
+
release: string;
|
|
30
|
+
sdk: { name: string; version: string };
|
|
31
|
+
tags: Record<string, string>;
|
|
32
|
+
user?: { id: string };
|
|
33
|
+
contexts: { app_info: StateSnapshot };
|
|
34
|
+
fingerprint: string[];
|
|
35
|
+
exception: { values: ExceptionValue[] };
|
|
36
|
+
breadcrumbs: { values: Breadcrumb[] };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ParsedDsn {
|
|
40
|
+
dsn: string;
|
|
41
|
+
endpoint: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const MAX_FRAMES = 50;
|
|
45
|
+
const DSN_PATTERN = /^(https?):\/\/([^:@/]+)(?::[^@/]*)?@([^/]+)\/(?:.+\/)?(\d+)$/;
|
|
46
|
+
const V8_FRAME = /^\s*at (?:(.+?) \()?(?:address at )?(.+?):(\d+):(\d+)\)?\s*$/;
|
|
47
|
+
const JSC_FRAME = /^(?:(.*?)@)?(.+?):(\d+):(\d+)\s*$/;
|
|
48
|
+
|
|
49
|
+
export function parseDsn(dsn: string): ParsedDsn | null {
|
|
50
|
+
const match = DSN_PATTERN.exec(dsn);
|
|
51
|
+
const protocol = match?.[1];
|
|
52
|
+
const publicKey = match?.[2];
|
|
53
|
+
const host = match?.[3];
|
|
54
|
+
const projectId = match?.[4];
|
|
55
|
+
if (protocol === undefined || publicKey === undefined || host === undefined || projectId === undefined) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return { dsn, endpoint: `${protocol}://${host}/api/${projectId}/envelope/?sentry_key=${publicKey}&sentry_version=7` };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function parseStack(stack: string | undefined): StackFrame[] {
|
|
62
|
+
const frames: StackFrame[] = [];
|
|
63
|
+
for (const line of (stack ?? '').split('\n')) {
|
|
64
|
+
const match = V8_FRAME.exec(line) ?? JSC_FRAME.exec(line);
|
|
65
|
+
const filename = match?.[2];
|
|
66
|
+
const lineno = match?.[3];
|
|
67
|
+
const colno = match?.[4];
|
|
68
|
+
if (filename === undefined || lineno === undefined || colno === undefined) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const name = match?.[1];
|
|
72
|
+
frames.push({ ...(name ? { function: name } : {}), filename, lineno: Number(lineno), colno: Number(colno) });
|
|
73
|
+
}
|
|
74
|
+
return frames.reverse().slice(-MAX_FRAMES);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function serializeEnvelope(dsn: string, event: SentryEvent, sentAt: string): string {
|
|
78
|
+
return [
|
|
79
|
+
JSON.stringify({ event_id: event.event_id, dsn, sent_at: sentAt }),
|
|
80
|
+
JSON.stringify({ type: 'event' }),
|
|
81
|
+
JSON.stringify(event),
|
|
82
|
+
].join('\n');
|
|
83
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { ApiRequestError, isIdentityRejection } from '../backend/errors.ts';
|
|
2
|
+
import { type Breadcrumb, parseDsn, type ParsedDsn, parseStack, type SentryEvent, serializeEnvelope, type StateSnapshot } from './envelope.ts';
|
|
3
|
+
import { scrubString } from './scrub.ts';
|
|
4
|
+
|
|
5
|
+
const MAX_SENDS_PER_SESSION = 50;
|
|
6
|
+
const MAX_BREADCRUMBS = 30;
|
|
7
|
+
const NETWORK_FAILURE = /network request failed|failed to fetch|network error|timed out/i;
|
|
8
|
+
|
|
9
|
+
export interface ReportIdentity {
|
|
10
|
+
sourceId: string | null;
|
|
11
|
+
sessionId: number | null;
|
|
12
|
+
userExternalId: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ReporterOptions {
|
|
16
|
+
dsn: string;
|
|
17
|
+
environment: string;
|
|
18
|
+
release: string;
|
|
19
|
+
sdkVersion: string;
|
|
20
|
+
send: (url: string, body: string) => Promise<unknown>;
|
|
21
|
+
now: () => number;
|
|
22
|
+
randomHex: (length: number) => string;
|
|
23
|
+
appInfo: () => StateSnapshot;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function describe(value: unknown): string {
|
|
27
|
+
if (typeof value === 'string') {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
return JSON.stringify(value) ?? String(value);
|
|
32
|
+
} catch {
|
|
33
|
+
return String(value);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function toError(context: string, error: unknown): Error {
|
|
38
|
+
if (error instanceof Error) {
|
|
39
|
+
return error;
|
|
40
|
+
}
|
|
41
|
+
return new Error(error === undefined ? context : `${context}: ${describe(error)}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function fingerprintOf(context: string, error: Error): string[] {
|
|
45
|
+
if (error instanceof ApiRequestError) {
|
|
46
|
+
return ['api-request-error', error.endpoint, String(error.status), error.code ?? ''];
|
|
47
|
+
}
|
|
48
|
+
return [context, error.name];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isNonActionable(error: unknown): error is Error {
|
|
52
|
+
if (error instanceof ApiRequestError) {
|
|
53
|
+
return error.kind !== 'http' || isIdentityRejection(error);
|
|
54
|
+
}
|
|
55
|
+
return error instanceof Error && NETWORK_FAILURE.test(error.message);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class Reporter {
|
|
59
|
+
private readonly target: ParsedDsn | null;
|
|
60
|
+
|
|
61
|
+
private readonly breadcrumbs: Breadcrumb[] = [];
|
|
62
|
+
|
|
63
|
+
private publishableKey: string | null = null;
|
|
64
|
+
|
|
65
|
+
private identityProvider: (() => ReportIdentity) | null = null;
|
|
66
|
+
|
|
67
|
+
private sent = 0;
|
|
68
|
+
|
|
69
|
+
private reporting = false;
|
|
70
|
+
|
|
71
|
+
constructor(private readonly options: ReporterOptions) {
|
|
72
|
+
this.target = parseDsn(options.dsn);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
setPublishableKey(publishableKey: string): void {
|
|
76
|
+
this.publishableKey = publishableKey;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
setIdentityProvider(provider: () => ReportIdentity): void {
|
|
80
|
+
this.identityProvider = provider;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
addBreadcrumb(category: string, message: string): void {
|
|
84
|
+
try {
|
|
85
|
+
this.breadcrumbs.push({ timestamp: this.options.now() / 1000, category, message: scrubString(message), level: 'info' });
|
|
86
|
+
if (this.breadcrumbs.length > MAX_BREADCRUMBS) {
|
|
87
|
+
this.breadcrumbs.shift();
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
report(context: string, error?: unknown): void {
|
|
95
|
+
if (this.reporting) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
this.reporting = true;
|
|
99
|
+
try {
|
|
100
|
+
if (isNonActionable(error)) {
|
|
101
|
+
this.addBreadcrumb(error instanceof ApiRequestError ? 'api' : 'network', `${context}: ${error.message}`);
|
|
102
|
+
} else {
|
|
103
|
+
this.capture(context, toError(context, error));
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
return;
|
|
107
|
+
} finally {
|
|
108
|
+
this.reporting = false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private capture(context: string, error: Error): void {
|
|
113
|
+
if (this.target === null || this.sent >= MAX_SENDS_PER_SESSION) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.sent += 1;
|
|
117
|
+
const { options } = this;
|
|
118
|
+
const appInfo = options.appInfo();
|
|
119
|
+
const identity = this.readIdentity();
|
|
120
|
+
const sessionId = identity?.sessionId ?? null;
|
|
121
|
+
const event: SentryEvent = {
|
|
122
|
+
event_id: options.randomHex(32),
|
|
123
|
+
timestamp: options.now() / 1000,
|
|
124
|
+
platform: 'javascript',
|
|
125
|
+
level: 'error',
|
|
126
|
+
environment: options.environment,
|
|
127
|
+
release: options.release,
|
|
128
|
+
sdk: { name: 'blar.react-native', version: options.sdkVersion },
|
|
129
|
+
tags: {
|
|
130
|
+
context: scrubString(context),
|
|
131
|
+
sdkVersion: options.sdkVersion,
|
|
132
|
+
publishableKey: this.publishableKey ?? 'unknown',
|
|
133
|
+
sourceId: identity?.sourceId ?? 'unknown',
|
|
134
|
+
sessionId: sessionId !== null ? String(sessionId) : 'none',
|
|
135
|
+
os: String(appInfo.os ?? 'unknown'),
|
|
136
|
+
},
|
|
137
|
+
...(identity?.userExternalId ? { user: { id: identity.userExternalId } } : {}),
|
|
138
|
+
contexts: { app_info: appInfo },
|
|
139
|
+
fingerprint: fingerprintOf(context, error),
|
|
140
|
+
exception: {
|
|
141
|
+
values: [
|
|
142
|
+
{
|
|
143
|
+
type: error.name,
|
|
144
|
+
value: scrubString(error.message),
|
|
145
|
+
stacktrace: { frames: parseStack(error.stack).map((frame) => ({ ...frame, filename: scrubString(frame.filename) })) },
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
breadcrumbs: { values: [...this.breadcrumbs] },
|
|
150
|
+
};
|
|
151
|
+
this.deliver(this.target, event);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private readIdentity(): ReportIdentity | null {
|
|
155
|
+
try {
|
|
156
|
+
return this.identityProvider?.() ?? null;
|
|
157
|
+
} catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private deliver(target: ParsedDsn, event: SentryEvent): void {
|
|
163
|
+
const body = serializeEnvelope(target.dsn, event, new Date(this.options.now()).toISOString());
|
|
164
|
+
try {
|
|
165
|
+
this.options.send(target.endpoint, body).catch(() => undefined);
|
|
166
|
+
} catch {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const EMAIL_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
|
|
2
|
+
const JWT_PATTERN = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*/g;
|
|
3
|
+
const BEARER_PATTERN = /\bBearer\s+[A-Za-z0-9._-]+/gi;
|
|
4
|
+
|
|
5
|
+
export function scrubString(value: string): string {
|
|
6
|
+
return value
|
|
7
|
+
.replace(EMAIL_PATTERN, '[redacted-email]')
|
|
8
|
+
.replace(JWT_PATTERN, '[redacted-token]')
|
|
9
|
+
.replace(BEARER_PATTERN, 'Bearer [redacted]');
|
|
10
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readAppInfo } from '../backend/app-info.ts';
|
|
2
|
+
import { SDK_VERSION } from '../version.ts';
|
|
3
|
+
import { SENTRY_DSN, SENTRY_ENVIRONMENT } from './build-info.generated.ts';
|
|
4
|
+
import { Reporter, type ReportIdentity } from './reporter.ts';
|
|
5
|
+
|
|
6
|
+
const HEX_DIGITS = 16;
|
|
7
|
+
|
|
8
|
+
function randomHex(length: number): string {
|
|
9
|
+
return Array.from({ length }, () => Math.floor(Math.random() * HEX_DIGITS).toString(HEX_DIGITS)).join('');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const reporter = new Reporter({
|
|
13
|
+
dsn: SENTRY_DSN,
|
|
14
|
+
environment: SENTRY_ENVIRONMENT,
|
|
15
|
+
release: `blario-react-native@${SDK_VERSION}`,
|
|
16
|
+
sdkVersion: SDK_VERSION,
|
|
17
|
+
send: (url, body) => fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/x-sentry-envelope' }, body }),
|
|
18
|
+
now: Date.now,
|
|
19
|
+
randomHex,
|
|
20
|
+
appInfo: () => ({ ...readAppInfo() }),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export function reportSdkError(context: string, error?: unknown): void {
|
|
24
|
+
reporter.report(context, error);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function recordSdkBreadcrumb(category: string, message: string): void {
|
|
28
|
+
reporter.addBreadcrumb(category, message);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function setReportPublishableKey(publishableKey: string): void {
|
|
32
|
+
reporter.setPublishableKey(publishableKey);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function setReportIdentityProvider(provider: () => ReportIdentity): void {
|
|
36
|
+
reporter.setIdentityProvider(provider);
|
|
37
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export interface ConnectionOptions {
|
|
2
|
+
publishableKey?: unknown;
|
|
3
|
+
apiBaseUrl?: unknown;
|
|
4
|
+
relayUrl?: unknown;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type ConnectionConfig =
|
|
8
|
+
| { kind: 'backend'; publishableKey: string; apiBaseUrl: string }
|
|
9
|
+
| { kind: 'relay'; relayUrl: string }
|
|
10
|
+
| { kind: 'disabled' };
|
|
11
|
+
|
|
12
|
+
function validHost(host: string): boolean {
|
|
13
|
+
if (host.startsWith('[')) {
|
|
14
|
+
const address = host.slice(1, -1);
|
|
15
|
+
const halves = address.split('::');
|
|
16
|
+
const groups = halves.flatMap((half) => half === '' ? [] : half.split(':'));
|
|
17
|
+
return halves.length <= 2 && groups.every((group) => /^[a-f0-9]{1,4}$/i.test(group)) &&
|
|
18
|
+
(halves.length === 2 ? groups.length < 8 : groups.length === 8);
|
|
19
|
+
}
|
|
20
|
+
if (/^[0-9.]+$/.test(host)) {
|
|
21
|
+
const octets = host.split('.');
|
|
22
|
+
return octets.length === 4 && octets.every((octet) => /^\d{1,3}$/.test(octet) && Number(octet) <= 255);
|
|
23
|
+
}
|
|
24
|
+
return host.replace(/\.$/, '').split('.').every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i.test(label));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeUrl(value: unknown, socket: boolean): string | null {
|
|
28
|
+
if (typeof value !== 'string') {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const match = /^([a-z]+):\/\/(\[[a-f0-9:]+\]|[a-z0-9.-]+)(?::([0-9]{1,5}))?((?:\/[^?#]*)?)(\?[^#]*)?$/i.exec(value.trim());
|
|
32
|
+
if (match === null) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const [, scheme = '', host = '', port, path = '', query = ''] = match;
|
|
36
|
+
const protocol = scheme.toLowerCase();
|
|
37
|
+
const allowed = socket ? ['ws', 'wss'] : ['http', 'https'];
|
|
38
|
+
const suffix = path + query;
|
|
39
|
+
if (!allowed.includes(protocol) || !validHost(host) ||
|
|
40
|
+
(port !== undefined && (Number(port) < 1 || Number(port) > 65535)) ||
|
|
41
|
+
(!socket && query !== '') || !/^[a-z0-9\-._~!$&'()*+,;=:@/%?]*$/i.test(suffix) || /%(?![a-f0-9]{2})/i.test(suffix)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return `${protocol}://${host.toLowerCase()}${port === undefined ? '' : `:${Number(port)}`}${socket ? suffix : path.replace(/\/+$/, '')}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function normalizeApiBaseUrl(value: unknown): string | null {
|
|
48
|
+
return normalizeUrl(value, false);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function normalizeRelayUrl(value: unknown): string | null {
|
|
52
|
+
return normalizeUrl(value, true);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function resolveConnectionOptions(options: ConnectionOptions, development: boolean): ConnectionConfig {
|
|
56
|
+
const { publishableKey, apiBaseUrl, relayUrl } = options;
|
|
57
|
+
if (publishableKey !== undefined || apiBaseUrl !== undefined) {
|
|
58
|
+
const base = normalizeApiBaseUrl(apiBaseUrl);
|
|
59
|
+
return typeof publishableKey === 'string' && publishableKey === publishableKey.trim() && /^[\x21-\x7e]+$/.test(publishableKey) && base !== null
|
|
60
|
+
? { kind: 'backend', publishableKey, apiBaseUrl: base }
|
|
61
|
+
: { kind: 'disabled' };
|
|
62
|
+
}
|
|
63
|
+
const relay = normalizeRelayUrl(relayUrl === undefined && development ? 'ws://localhost:8790' : relayUrl);
|
|
64
|
+
return relay === null ? { kind: 'disabled' } : { kind: 'relay', relayUrl: relay };
|
|
65
|
+
}
|