Diagnostics for atproto PDS hosts, DIDs, and handles: https://debug.hose.cam
15
fork

Configure Feed

Select the types of activity you want to include in your feed.

at c1ea8588cdc19e86c8bd3fa35043cb4f6266def5 524 lines 19 kB view raw
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 { 12 Client, 13 ClientResponseError, 14 ok, 15 simpleFetchHandler, 16 } from 'https://esm.sh/@atcute/client@4.1.1'; 17 import { 18 DohJsonHandleResolver, 19 WellKnownHandleResolver, 20 } from 'https://esm.sh/@atcute/identity-resolver@1.2.0'; 21 22 window.SimpleQuery = service => { 23 const client = new Client({ handler: simpleFetchHandler({ service }) }); 24 return (...args) => ok(client.get(...args)); 25 }; 26 window.SimpleProc = service => { 27 const client = new Client({ handler: simpleFetchHandler({ service }) }); 28 return (...args) => ok(client.post(...args)); 29 }; 30 window.isXrpcErr = e => e instanceof ClientResponseError; 31 32 window.dnsResolver = new DohJsonHandleResolver({ 33 dohUrl: 'https://mozilla.cloudflare-dns.com/dns-query', 34 }); 35 window.httpResolver = new WellKnownHandleResolver(); 36 37 window.slingshot = window.SimpleQuery('https://slingshot.microcosm.blue'); 38 window.relays = [ 39 { 40 name: 'Bluesky production', 41 hostname: 'bsky.network', 42 }, 43 { 44 name: 'Bluesky sync1.1 East', 45 hostname: 'relay1.us-east.bsky.network', 46 }, 47 { 48 name: 'Bluesky sync1.1 West', 49 hostname: 'relay1.us-west.bsky.network', 50 }, 51 { 52 name: 'Blacksky', 53 hostname: 'atproto.africa', 54 }, 55 { 56 name: 'Microcosm Montreal', 57 hostname: 'relay.fire.hose.cam', 58 }, 59 { 60 name: 'Microcosm France', 61 hostname: 'relay3.fr.hose.cam', 62 }, 63 ]; 64 </script> 65 66 <script> 67 document.addEventListener('alpine:init', () => { 68 Alpine.data('debug', () => ({ 69 // form input 70 identifier: '', 71 72 // state 73 identifierLoading: false, 74 identifierError: null, 75 76 // stuff to check 77 pds: null, 78 did: null, 79 handle: null, 80 81 async goto(identifier) { 82 this.identifier = identifier; 83 await this.diagnose(); 84 }, 85 86 async diagnose() { 87 this.identifierLoading = true; 88 this.identifierError = null; 89 this.pds = null; 90 this.did = null; 91 this.handle = null; 92 this.identifier = this.identifier.trim(); 93 if (this.identifier === '') { 94 // do nothing 95 } else if (this.identifier.startsWith('https://')) { 96 this.pds = this.identifier; 97 } else { 98 if (this.identifier.startsWith('at://')) { 99 this.identifier = this.identifier.slice('at://'.length); 100 } 101 if (this.identifier.startsWith('did:')) { 102 this.did = this.identifier; 103 let data; 104 try { 105 data = await window.slingshot('com.bad-example.identity.resolveMiniDoc', { 106 params: { identifier: this.identifier }, 107 }); 108 this.pds = data.pds; 109 this.handle = data.handle; 110 } catch (e) { 111 if (window.isXrpcErr(e)) { 112 this.identifierError = e.error; 113 if (e.message) this.description += ` ${e.description}`; 114 } else { 115 this.identifierError = 'Failed to resolve identifier, see console for error.'; 116 console.error(e); 117 } 118 } 119 } else { 120 this.handle = this.identifier; 121 let data; 122 try { 123 data = await window.slingshot('com.bad-example.identity.resolveMiniDoc', { 124 params: { identifier: this.identifier }, 125 }); 126 this.did = data.did; 127 this.pds = data.pds; 128 } catch (e) { 129 if (window.isXrpcErr(e)) { 130 this.identifierError = e.error; 131 if (e.message) this.description += ` ${e.description}`; 132 } else { 133 this.identifierError = 'Failed to resolve identifier, see console for error.'; 134 console.error(e); 135 } 136 } 137 } 138 } 139 this.identifierLoading = false; 140 }, 141 })); 142 143 Alpine.data('pdsCheck', pds => ({ 144 loadingDesc: false, 145 error: null, 146 description: null, 147 accounts: [], 148 accountsComplete: false, 149 150 async init() { 151 await this.update(pds); 152 }, 153 154 async update(pds) { 155 this.loadingDesc = true; 156 this.error = null; 157 this.description = null; 158 this.accounts = []; 159 let query = window.SimpleQuery(pds); 160 try { 161 this.description = await query('com.atproto.server.describeServer'); 162 } catch (e) { 163 if (window.isXrpcErr(e)) { 164 this.error = e.error; 165 } else { 166 this.error = 'Failed to reach (see console)'; 167 console.error(e); 168 } 169 } 170 let accountsRes; 171 try { 172 accountsRes = await query('com.atproto.sync.listRepos', { 173 params: { limit: 7 }, 174 }); 175 this.accounts = accountsRes.repos; 176 this.accountsComplete == !accountsRes.cursor; 177 } catch (e) { 178 if (window.isXrpcErr(e)) { 179 this.error = e.error; 180 } else { 181 this.error = 'Failed to reach (see console)'; 182 console.error(e); 183 } 184 } 185 this.loadingDesc = false; 186 }, 187 })); 188 189 Alpine.data('relayCheckHost', (pds, relay) => ({ 190 loading: false, 191 error: null, 192 status: null, 193 reqCrawlStatus: null, 194 reqCrawlError: null, 195 196 async init() { 197 await this.check(pds, relay); 198 }, 199 200 async check(pds, relay) { 201 this.loading = true; 202 this.error = null; 203 this.status = null; 204 const query = window.SimpleQuery(`https://${relay.hostname}`); 205 const hostname = pds.split('://')[1]; 206 let data; 207 try { 208 data = await query('com.atproto.sync.getHostStatus', { 209 params: { hostname }, 210 }); 211 this.status = data.status; 212 } catch(e) { 213 if (window.isXrpcErr(e)) { 214 this.error = e.error; 215 } else { 216 this.error = 'Failed to check (see console)'; 217 console.error(e); 218 } 219 } 220 this.loading = false; 221 this.reqCrawlStatus = null; 222 this.reqCrawlError = null; 223 }, 224 225 async requestCrawl(pds, relay) { 226 this.reqCrawlStatus = "loading"; 227 const proc = window.SimpleProc(`https://${relay.hostname}`); 228 const hostname = pds.split('://')[1]; 229 let data; 230 try { 231 data = await proc('com.atproto.sync.requestCrawl', { 232 input: { hostname }, 233 }); 234 } catch (e) { 235 if (window.isXrpcErr(e)) { 236 this.reqCrawlError = e.error; 237 } else { 238 this.reqCrawlError = 'failed (see console)'; 239 console.error(e); 240 } 241 } 242 this.reqCrawlStatus = "done"; 243 }, 244 })); 245 246 Alpine.data('checkHandle', handle => ({ 247 loading: false, 248 dnsDid: null, 249 dnsErr: null, 250 httpDid: null, 251 httpErr: null, 252 253 async init() { 254 await this.updateHandle(handle); 255 }, 256 async updateHandle(handle) { 257 this.loading = true; 258 this.dnsDid = null; 259 this.dnsErr = null; 260 this.httpDid = null; 261 this.httpErr = null; 262 try { 263 this.dnsDid = await window.dnsResolver.resolve(handle); 264 } catch (e) { 265 this.dnsErr = e.name; 266 } 267 try { 268 this.httpDid = await window.httpResolver.resolve(handle); 269 } catch (e) { 270 this.httpErr = e.name; 271 } 272 this.loading = false; 273 }, 274 })); 275 }) 276 </script> 277 </head> 278 <body x-data="debug"> 279 <div class="hero bg-base-200"> 280 <div class="hero-content flex-col"> 281 <h1>PDS Debugger</h1> 282 283 <p>Work in progress!</p> 284 <details class="text-xs"> 285 <summary>Would be nice</summary> 286 <ul> 287 <li>anything that actually works</li> 288 <li>firehose listener for missing pds events</li> 289 <li>jetstream listener for missing pds events</li> 290 <li>check relays for account status</li> 291 <li>check relays for pds state</li> 292 <li>plc: check old pds hosts for active account state</li> 293 </ul> 294 </details> 295 <details class="text-xs"> 296 <summary>Limitations</summary> 297 <ul> 298 <li>it's all client-side</li> 299 </ul> 300 </details> 301 302 <div class="card bg-base-100 w-full max-w-sm shrink-0 shadow-2xl"> 303 <div class="card-body"> 304 <form @submit.prevent="await diagnose()"> 305 <label> 306 Enter an atproto handle, DID, or HTTPS PDS URL 307 <input 308 class="input" 309 x-model="identifier" 310 :disabled="identifierLoading" 311 autofocus 312 /> 313 </label> 314 </form> 315 </div> 316 </div> 317 318 <template x-if="identifierError"> 319 <p>uh oh: <span x-text="identifierError"></span></p> 320 </template> 321 322 <template x-if="pds != null"> 323 <div class="card bg-base-100 w-full max-w-lg shrink-0 shadow-2xl"> 324 <div class="card-body"> 325 <h2 class="card-title"> 326 <span class="badge badge-secondary">PDS</span> 327 <span x-text="pds"></span> 328 </h2> 329 330 <div 331 x-data="pdsCheck(pds)" 332 x-init="$watch('pds', v => update(v))" 333 > 334 <h3 class="text-lg"> 335 Server 336 <span 337 x-show="description !== null" 338 class="badge badge-sm badge-soft badge-success" 339 >online</span> 340 </h3> 341 <p x-show="loadingDesc">Loading&hellip;</p> 342 <p x-show="error" class="text-warning" x-text="error"></p> 343 <template x-if="description !== null"> 344 <div class="overflow-x-auto"> 345 <table class="table table-xs"> 346 <tbody> 347 <tr> 348 <td class="text-sm">Open registration</td> 349 <td 350 class="text-sm" 351 x-text="!description.inviteCodeRequired" 352 ></td> 353 </tr> 354 </tbody> 355 </table> 356 <h4 class="font-bold"> 357 Accounts 358 </h4> 359 <table class="table table-xs"> 360 <tbody> 361 <template x-for="account in accounts"> 362 <tr> 363 <td> 364 <code> 365 <a 366 href="#" 367 class="link" 368 x-text="account.did" 369 @click.prevent="goto(account.did)" 370 ></a> 371 </code> 372 </td> 373 <td> 374 <span 375 x-show="account.active" 376 class="badge badge-sm badge-soft badge-success" 377 > 378 active 379 </span> 380 <span 381 x-show="!account.active" 382 x-text="account.status" 383 class="badge badge-sm badge-soft badge-warning" 384 ></span> 385 </td> 386 </tr> 387 </template> 388 <template x-if="!accountsComplete"> 389 <tr> 390 <td colspan="2" class="text-sm text-warning-content"> 391 (account list clipped) 392 </td> 393 </tr> 394 </template> 395 </tbody> 396 </table> 397 </div> 398 </template> 399 </div> 400 401 <h3 class="text-lg">Relay host status</h3> 402 <div class="overflow-x-auto"> 403 <table class="table table-xs"> 404 <tbody> 405 <template x-for="relay in window.relays"> 406 <tr 407 x-data="relayCheckHost(pds, relay)" 408 x-init="$watch('pds', pds => check(pds, relay))" 409 > 410 <td x-text="relay.name" class="text-sm"></td> 411 <td> 412 <template x-if="loading"> 413 <em>loading&hellip;</em> 414 </template> 415 <template x-if="error"> 416 <span 417 x-text="error" 418 class="text-xs text-warning" 419 ></span> 420 </template> 421 <template x-if="status"> 422 <span 423 x-text="status" 424 class="badge badge-sm" 425 :class="status === 'active' && 'badge-soft badge-success'" 426 ></span> 427 </template> 428 </td> 429 <td> 430 <div x-show="status !== 'active'"> 431 <button 432 x-show="reqCrawlStatus !== 'done'" 433 class="btn btn-xs btn-ghost whitespace-nowrap" 434 :disabled="reqCrawlStatus === 'loading'" 435 @click="requestCrawl(pds, relay)" 436 > 437 request crawl 438 </button> 439 <span 440 x-show="reqCrawlError !== null" 441 x-text="reqCrawlError" 442 class="text-xs text-warning" 443 ></span> 444 <button 445 x-show="reqCrawlError === null && reqCrawlStatus === 'done'" 446 class="btn btn-xs btn-soft btn-primary whitespace-nowrap" 447 @click="check" 448 > 449 refresh 450 </button> 451 </div> 452 </td> 453 </tr> 454 </template> 455 </tbody> 456 </table> 457 </div> 458 </div> 459 </div> 460 </template> 461 462 <template x-if="did != null"> 463 <div class="card bg-base-100 w-full max-w-lg shrink-0 shadow-2xl"> 464 <div class="card-body"> 465 <h2 class="card-title"> 466 <span class="badge badge-secondary">DID</span> 467 <code x-text="did"></code> 468 </h2> 469 <p>(wip)</p> 470 </div> 471 </div> 472 </template> 473 474 <template x-if="handle != null"> 475 <div class="card bg-base-100 w-full max-w-lg shrink-0 shadow-2xl"> 476 <div 477 x-data="checkHandle(handle)" 478 x-init="$watch('handle', h => updateHandle(h))" 479 class="card-body" 480 > 481 <h2 class="card-title"> 482 <span class="badge badge-secondary">Handle</span> 483 <span x-text="handle"></span> 484 </h2> 485 <p x-show="loading" class="text-i">Loading&hellip;</p> 486 <div x-show="!loading" class="overflow-x-auto"> 487 <table class="table table-xs"> 488 <tbody> 489 <tr> 490 <td class="text-sm">DNS</td> 491 <td class="text-sm"> 492 <code x-text="dnsDid"></code> 493 </td> 494 <td> 495 <div 496 class="badge badge-sm badge-soft badge-neutral" 497 x-show="dnsErr !== null" 498 x-text="dnsErr" 499 ></div> 500 </td> 501 </tr> 502 <tr> 503 <td class="text-sm">Http</td> 504 <td class="text-sm"> 505 <code x-text="httpDid"></code> 506 </td> 507 <td> 508 <div 509 class="badge badge-sm badge-soft badge-neutral" 510 x-show="httpErr !== null" 511 x-text="httpErr" 512 ></div> 513 </td> 514 </tr> 515 </tbody> 516 </table> 517 </div> 518 </div> 519 </div> 520 </template> 521 </div> 522 </div> 523 </body> 524</html>