Diagnostics for atproto PDS hosts, DIDs, and handles: https://debug.hose.cam
1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="utf-8">
5 <meta name="viewport" content="width=device-width"/>
6 <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
7 <link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css"/>
8 <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
9
10 <script type="module">
11 import { Client, ClientResponseError, ok, simpleFetchHandler } from 'https://esm.sh/@atcute/client@4.1.1';
12 window.SimpleQuery = service => {
13 const client = new Client({ handler: simpleFetchHandler({ service }) });
14 return (...args) => ok(client.get(...args));
15 };
16 window.SimpleProc = service => {
17 const client = new Client({ handler: simpleFetchHandler({ service }) });
18 return (...args) => ok(client.post(...args));
19 };
20 window.isXrpcErr = e => e instanceof ClientResponseError;
21 window.slingshot = window.SimpleQuery('https://slingshot.microcosm.blue');
22 window.relays = [
23 {
24 name: 'Bluesky production',
25 hostname: 'bsky.network',
26 },
27 {
28 name: 'Bluesky sync1.1 East',
29 hostname: 'relay1.us-east.bsky.network',
30 },
31 {
32 name: 'Bluesky sync1.1 West',
33 hostname: 'relay1.us-west.bsky.network',
34 },
35 {
36 name: 'Blacksky',
37 hostname: 'atproto.africa',
38 },
39 {
40 name: 'Microcosm Montreal',
41 hostname: 'relay.fire.hose.cam',
42 },
43 {
44 name: 'Microcosm France',
45 hostname: 'relay3.fr.hose.cam',
46 },
47 ];
48 </script>
49
50 <script>
51 document.addEventListener('alpine:init', () => {
52 Alpine.data('debug', () => ({
53 // form input
54 identifier: '',
55
56 // state
57 identifierLoading: false,
58 identifierError: null,
59
60 // stuff to check
61 pds: null,
62 did: null,
63 handle: null,
64
65 async diagnose() {
66 this.identifierLoading = true;
67 this.identifierError = null;
68 this.pds = null;
69 this.did = null;
70 if (this.identifier.startsWith('https://')) {
71 this.pds = this.identifier;
72 } else {
73 if (this.identifier.startsWith('at://')) {
74 this.identifier = this.identifier.slice('at://'.length);
75 }
76 if (this.identifier.startsWith('did:')) {
77 this.did = this.identifier;
78 } else {
79 let data;
80 try {
81 data = await window.slingshot('com.bad-example.identity.resolveMiniDoc', {
82 params: { identifier: this.identifier },
83 });
84 this.did = data.did;
85 this.pds = data.pds;
86 } catch (e) {
87 if (window.isXrpcErr(e)) {
88 this.identifierError = e.error;
89 if (e.message) this.description += ` ${e.description}`;
90 } else {
91 this.identifierError = 'Failed to resolve identifier, see console for error.';
92 console.error(e);
93 }
94 }
95 }
96 }
97 this.identifierLoading = false;
98 },
99 }));
100
101 Alpine.data('relayCheckHost', (pds, relay) => ({
102 loading: false,
103 error: null,
104 status: null,
105 reqCrawlStatus: null,
106 reqCrawlError: null,
107
108 async init() {
109 await this.check();
110 },
111
112 async check() {
113 this.loading = true;
114 this.error = null;
115 this.status = null;
116 let query = window.SimpleQuery(`https://${relay.hostname}`);
117 const hostname = pds.split('://')[1];
118 let data;
119 try {
120 data = await query('com.atproto.sync.getHostStatus', {
121 params: { hostname },
122 });
123 this.status = data.status;
124 } catch(e) {
125 if (window.isXrpcErr(e)) {
126 this.error = e.error;
127 } else {
128 this.error = 'Failed to check (see console)';
129 console.error(e);
130 }
131 }
132 this.loading = false;
133 this.reqCrawlStatus = null;
134 this.reqCrawlError = null;
135 },
136
137 async requestCrawl() {
138 this.reqCrawlStatus = "loading";
139 const proc = window.SimpleProc(`https://${relay.hostname}`);
140 const hostname = pds.split('://')[1];
141 let data;
142 try {
143 data = await proc('com.atproto.sync.requestCrawl', {
144 input: { hostname },
145 });
146 } catch (e) {
147 if (window.isXrpcErr(e)) {
148 this.reqCrawlError = e.error;
149 } else {
150 this.reqCrawlError = 'failed (see console)';
151 console.error(e);
152 }
153 }
154 this.reqCrawlStatus = "done";
155 },
156 }))
157 })
158 </script>
159 </head>
160 <body x-data="debug">
161 <div class="hero bg-base-200">
162 <div class="hero-content flex-col">
163 <h1>PDS Debugger</h1>
164
165 <p>Work in progress!</p>
166 <details class="text-xs">
167 <summary>Would be nice</summary>
168 <ul>
169 <li>anything that actually works</li>
170 <li>firehose listener for missing pds events</li>
171 <li>jetstream listener for missing pds events</li>
172 <li>check relays for account status</li>
173 <li>check relays for pds state</li>
174 <li>plc: check old pds hosts for active account state</li>
175 </ul>
176 </details>
177 <details class="text-xs">
178 <summary>Limitations</summary>
179 <ul>
180 <li>it's all client-side</li>
181 </ul>
182 </details>
183
184 <div class="card bg-base-100 w-full max-w-sm shrink-0 shadow-2xl">
185 <div class="card-body">
186 <form @submit.prevent="await diagnose()">
187 <label>
188 Enter an atproto handle, DID, or HTTPS PDS URL
189 <input
190 class="input"
191 x-model="identifier"
192 :disabled="identifierLoading"
193 autofocus
194 />
195 </label>
196 </form>
197 </div>
198 </div>
199
200 <template x-if="identifierError">
201 <p>uh oh: <span x-text="identifierError"></span></p>
202 </template>
203
204 <template x-if="pds != null">
205 <div class="card bg-base-100 w-full max-w-lg shrink-0 shadow-2xl">
206 <div class="card-body">
207 <h2 class="card-title">
208 <span class="badge badge-secondary">PDS</span>
209 <span x-text="pds"></span>
210 </h2>
211
212 <h3 class="text-lg">Relay host status</h3>
213 <div class="overflow-x-auto">
214 <table class="table table-xs">
215 <tbody>
216 <template x-for="relay in window.relays">
217 <tr x-data="relayCheckHost(pds, relay)">
218 <td x-text="relay.name" class="text-sm"></td>
219 <td>
220 <template x-if="loading">
221 <em>loading…</em>
222 </template>
223 <template x-if="error">
224 <span
225 x-text="error"
226 class="text-xs text-warning"
227 ></span>
228 </template>
229 <template x-if="status">
230 <span
231 x-text="status"
232 class="badge badge-sm"
233 :class="status === 'active' && 'badge-soft badge-success'"
234 ></span>
235 </template>
236 </td>
237 <td>
238 <div x-show="status !== 'active'">
239 <button
240 x-show="reqCrawlStatus !== 'done'"
241 class="btn btn-xs btn-ghost whitespace-nowrap"
242 :disabled="reqCrawlStatus === 'loading'"
243 @click="requestCrawl"
244 >
245 request crawl
246 </button>
247 <span
248 x-show="reqCrawlError !== null"
249 x-text="reqCrawlError"
250 class="text-xs text-warning"
251 ></span>
252 <button
253 x-show="reqCrawlError === null && reqCrawlStatus === 'done'"
254 class="btn btn-xs btn-soft btn-primary whitespace-nowrap"
255 @click="check"
256 >
257 refresh
258 </button>
259 </div>
260 </td>
261 </tr>
262 </template>
263 </tbody>
264 </table>
265 </div>
266 </div>
267 </div>
268 </template>
269
270 <template x-if="did != null">
271 <div class="card bg-base-100 w-full max-w-sm shrink-0 shadow-2xl">
272 <div class="card-body">
273 <p x-text="`DID: ${did}`"></p>
274 </div>
275 </div>
276 </template>
277 </div>
278 </div>
279 </body>
280</html>