Skip to content

Commit b47420b

Browse files
authored
Surface risk intelligence data in playground (#108)
* surface risk intelligence data in playground * make subset disclaimer more prominent
1 parent cf6b0d4 commit b47420b

6 files changed

Lines changed: 516 additions & 17 deletions

File tree

package-lock.json

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"@docusaurus/plugin-client-redirects": "^3.7.0",
2020
"@docusaurus/preset-classic": "^3.7.0",
2121
"@easyops-cn/docusaurus-search-local": "^0.45.0",
22+
"@friendlycaptcha/server-sdk": "^0.3.0",
2223
"@iconify/react": "^6.0.2",
2324
"@mdx-js/react": "^3.0.1",
2425
"clsx": "^1.2.1",

src/components/playground/PlaygroundConfigEditor.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -231,26 +231,26 @@ export default function PlaygroundConfigEditor({
231231
</div>
232232
</div>
233233

234-
{/* Simulate False Positive Toggle */}
234+
{/* Simulate Suspicious User Toggle */}
235235
<div className="mb-6">
236236
<label className="flex items-center">
237237
<input
238238
type="checkbox"
239-
checked={settings.simulateFalsePositive}
239+
checked={settings.simulateHighRisk}
240240
onChange={(e) =>
241241
setSettings({
242242
...settings,
243-
simulateFalsePositive: e.target.checked,
243+
simulateHighRisk: e.target.checked,
244244
})
245245
}
246246
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
247247
/>
248248
<span className="ml-2 text-sm text-gray-700 dark:text-gray-300">
249-
Simulate high solving time
249+
Simulate suspicious user
250250
</span>
251251
</label>
252252
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1.5">
253-
{settings.simulateFalsePositive
253+
{settings.simulateHighRisk
254254
? "The widget will simulate that the user is suspicious which results in a high solving time"
255255
: "The widget will behave normally"}
256256
</div>
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
import { RiskIntelligenceData } from "@site/src/lib/playground";
2+
import React, { useState } from "react";
3+
4+
export interface PlaygroundSubmitDialogProps {
5+
type: "success" | "error";
6+
message: string;
7+
riskIntelligence?: RiskIntelligenceData;
8+
onClose: () => void;
9+
}
10+
11+
function RiskScoreBadge({ score }: { score: number }) {
12+
const colors = {
13+
0: "bg-gray-200 text-gray-700 dark:bg-gray-600 dark:text-gray-300",
14+
1: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
15+
2: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
16+
3: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200",
17+
4: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
18+
5: "bg-red-200 text-red-900 dark:bg-red-950 dark:text-red-200",
19+
};
20+
const labels = {
21+
0: "Unknown",
22+
1: "Very Low",
23+
2: "Low",
24+
3: "Medium",
25+
4: "High",
26+
5: "Very High",
27+
};
28+
return (
29+
<span
30+
className={`inline-block px-2 py-0.5 rounded text-xs font-medium ${
31+
colors[score] ?? colors[0]
32+
}`}
33+
>
34+
{score}/5 — {labels[score] ?? "Unknown"}
35+
</span>
36+
);
37+
}
38+
39+
function RiskIntelligenceSection({ data }: { data: any }) {
40+
const [viewMode, setViewMode] = useState<"pretty" | "raw">("pretty");
41+
const { risk_scores, network, client } = data;
42+
43+
return (
44+
<div className="mt-4 border-t border-gray-200 dark:border-gray-700 pt-4">
45+
<div className="flex items-center justify-between mb-2">
46+
<div className="text-sm font-semibold text-gray-900 dark:text-white">
47+
Risk Intelligence
48+
</div>
49+
<div className="flex rounded-md overflow-hidden border border-gray-300 dark:border-gray-600 text-xs">
50+
<button
51+
onClick={() => setViewMode("pretty")}
52+
className={`px-2 py-1 ${
53+
viewMode === "pretty"
54+
? "bg-blue-600 text-white"
55+
: "bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300"
56+
}`}
57+
>
58+
Pretty
59+
</button>
60+
<button
61+
onClick={() => setViewMode("raw")}
62+
className={`px-2 py-1 ${
63+
viewMode === "raw"
64+
? "bg-blue-600 text-white"
65+
: "bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300"
66+
}`}
67+
>
68+
Raw
69+
</button>
70+
</div>
71+
</div>
72+
<p className="text-sm text-gray-500 dark:text-gray-400 mb-3">
73+
<a
74+
href="https://developer.friendlycaptcha.com/docs/v2/risk-intelligence/"
75+
target="_blank"
76+
rel="noopener noreferrer"
77+
className="underline hover:text-blue-600 dark:hover:text-blue-400"
78+
>
79+
Risk Intelligence
80+
</a>{" "}
81+
provides risk scores and signals about the risk associated with a user.
82+
The data shown here is simulated for the playground and does not reflect
83+
reality.
84+
</p>
85+
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
86+
Risk Intelligence data is available to all{" "}
87+
<span className="font-medium">Advanced</span> and{" "}
88+
<span className="font-medium">Enterprise</span> customers.
89+
</p>
90+
91+
{viewMode === "raw" ? (
92+
<pre className="bg-gray-50 dark:bg-gray-900 rounded p-3 text-xs overflow-x-auto text-gray-800 dark:text-gray-200 font-mono">
93+
{JSON.stringify(data, null, 2)}
94+
</pre>
95+
) : (
96+
<>
97+
{/* Risk Scores */}
98+
<div className="grid grid-cols-3 gap-2 mb-5">
99+
{[
100+
{ label: "Overall", score: risk_scores.overall },
101+
{ label: "Network", score: risk_scores.network },
102+
{ label: "Browser", score: risk_scores.browser },
103+
].map(({ label, score }) => (
104+
<div
105+
key={label}
106+
className="text-center p-2 bg-gray-50 dark:bg-gray-750 rounded"
107+
>
108+
<div className="text-xs text-gray-600 dark:text-gray-300 mb-1">
109+
{label}
110+
</div>
111+
<RiskScoreBadge score={score} />
112+
</div>
113+
))}
114+
</div>
115+
116+
{/* Network Info */}
117+
<div className="space-y-5 text-xs">
118+
<div>
119+
<div className="font-bold text-base text-gray-800 dark:text-gray-300 mb-2">
120+
Network
121+
</div>
122+
<p className="text-gray-500 dark:text-gray-400">
123+
This only shows a subset of available information, switch to Raw
124+
for full data.
125+
</p>
126+
<div className="bg-gray-50 dark:bg-gray-750 rounded p-2 space-y-2">
127+
<Row label="IP" value={network.ip} />
128+
<Row
129+
label="ASN"
130+
value={`${network.as.name} (${network.as.type})`}
131+
/>
132+
<Row
133+
label="Location"
134+
value={`${network.geolocation.city}, ${network.geolocation.country.name}`}
135+
/>
136+
{(network.anonymization.vpn_score > 0 ||
137+
network.anonymization.proxy_score > 0 ||
138+
network.anonymization.tor) && (
139+
<div className="pt-2 border-t border-gray-200 dark:border-gray-600 mt-1 space-y-2">
140+
{network.anonymization.vpn_score > 0 && (
141+
<Row
142+
label="VPN"
143+
value={
144+
<RiskScoreBadge
145+
score={network.anonymization.vpn_score}
146+
/>
147+
}
148+
/>
149+
)}
150+
{network.anonymization.proxy_score > 0 && (
151+
<Row
152+
label="Proxy"
153+
value={
154+
<RiskScoreBadge
155+
score={network.anonymization.proxy_score}
156+
/>
157+
}
158+
/>
159+
)}
160+
{network.anonymization.tor && (
161+
<Row label="Tor" value="Yes" />
162+
)}
163+
</div>
164+
)}
165+
</div>
166+
</div>
167+
168+
{/* Client Info */}
169+
<div>
170+
<div className="font-bold text-base text-gray-800 dark:text-gray-300 mb-2">
171+
Client
172+
</div>
173+
<p className="text-gray-500 dark:text-gray-400">
174+
This only shows a subset of available information, switch to Raw
175+
for full data.
176+
</p>
177+
<div className="bg-gray-50 dark:bg-gray-750 rounded p-2 space-y-2">
178+
<Row
179+
label="Browser"
180+
value={`${client.browser.name} ${client.browser.version}`}
181+
/>
182+
<Row
183+
label="OS"
184+
value={`${client.os.name} ${client.os.version}`}
185+
/>
186+
<Row label="Device" value={client.device.type} />
187+
<Row label="Timezone" value={client.time_zone.name} />
188+
{client.automation.automation_tool.detected && (
189+
<div className="pt-2 border-t border-gray-200 dark:border-gray-600 mt-1">
190+
<Row
191+
label="Automation"
192+
value={
193+
<span className="text-red-600 dark:text-red-400 font-medium">
194+
{client.automation.automation_tool.name} detected
195+
</span>
196+
}
197+
/>
198+
</div>
199+
)}
200+
{client.automation.known_bot.detected && (
201+
<Row
202+
label="Bot"
203+
value={
204+
<span className="text-red-600 dark:text-red-400 font-medium">
205+
{client.automation.known_bot.name} (
206+
{client.automation.known_bot.type})
207+
</span>
208+
}
209+
/>
210+
)}
211+
</div>
212+
</div>
213+
</div>
214+
</>
215+
)}
216+
</div>
217+
);
218+
}
219+
220+
function Row({ label, value }: { label: string; value: React.ReactNode }) {
221+
return (
222+
<div className="flex justify-between gap-2">
223+
<span className="text-gray-600 dark:text-gray-300 shrink-0">{label}</span>
224+
<span className="text-gray-900 dark:text-gray-100 text-right">
225+
{value}
226+
</span>
227+
</div>
228+
);
229+
}
230+
231+
export default function PlaygroundSubmitDialog({
232+
type,
233+
message,
234+
riskIntelligence,
235+
onClose,
236+
}: PlaygroundSubmitDialogProps) {
237+
return (
238+
<div
239+
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
240+
onClick={onClose}
241+
>
242+
<div
243+
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto"
244+
onClick={(e) => e.stopPropagation()}
245+
>
246+
<div className="flex items-center gap-3 mb-3">
247+
{type === "success" ? (
248+
<span className="text-green-600 text-2xl">&#10003;</span>
249+
) : (
250+
<span className="text-red-600 text-2xl">&#10007;</span>
251+
)}
252+
<div className="text-lg font-semibold text-gray-900 dark:text-white">
253+
{type === "success" ? "Success" : "Error"}
254+
</div>
255+
</div>
256+
<p className="text-gray-700 dark:text-gray-300">{message}</p>
257+
258+
{riskIntelligence && (
259+
<RiskIntelligenceSection data={riskIntelligence} />
260+
)}
261+
262+
<div className="flex justify-end mt-4">
263+
<button
264+
onClick={onClose}
265+
className="px-4 py-2 text-sm bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-md hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
266+
>
267+
Dismiss
268+
</button>
269+
</div>
270+
</div>
271+
</div>
272+
);
273+
}

0 commit comments

Comments
 (0)