Skip to main content

sds200-python

sds200-python logo

CI Python 3.11–3.14 Development status: alpha License: MIT

Python control and monitoring library for the Uniden SDS100, SDS150, and SDS200 scanners. All three models support USB serial control; the SDS200 also supports native Ethernet control.

The project provides a typed Python API and an sdsctl command-line tool for scanner discovery, status monitoring, commands, connection profiles, diagnostics, and live state updates.

[!IMPORTANT] This project is alpha software. The public API may change before version 1.0. It is not affiliated with or endorsed by Uniden.

Interface preview

The sdsctl Textual TUI showing scanner state, audio controls, recording status, and operational logs populated with fictional demonstration data

The current Textual TUI rendered by the real application with fictional demonstration data. No scanner, agency, channel, endpoint, or recording information in this image represents a real system.

Features

  • USB serial control for SDS100, SDS150, and SDS200 scanners
  • Native SDS200 Ethernet control over UDP
  • Model detection, aliases, capability reporting, and model-specific limits
  • Model-aware handheld battery reporting: optional SDS100 GSI telemetry and SDS150 GCS charge status
  • Automatic USB and bounded LAN discovery
  • Saved serial, network, and automatic fallback profiles
  • Preferred transport ordering with live USB/Ethernet failover and opt-in recovery
  • Typed commands and responses, including documented hold/next/previous navigation
  • Structured GSI and continuous PSI scanner information
  • Thread-safe synchronized radio state and change events
  • Live terminal monitoring
  • Optional responsive Textual full-screen TUI for Raspberry Pi and terminal use with non-blocking scanner and SDS200 audio-recording controls
  • Exponential reconnect backoff with configurable retry limits
  • Traffic tracing, replayable JSON Lines session capture, and deterministic replay
  • Bounded health history plus failover and preferred-recovery diagnostics
  • Configurable operational logging to stderr, journald, or a logrotate-managed file
  • Automatic rate-limited recovery from a connected-but-stale TUI PSI stream
  • JSON Lines events for connection, retry, failover, and state changes
  • Discovery-based repair for stale USB paths and scanner IP addresses
  • Hardware-validated SDS200 network audio over RTSP/RTP
  • Native G.711 mu-law decoding with independently buffered PCM destinations
  • Versioned Broadcastify destination profiles with environment-backed secret references and validated adapter conversion
  • Optional renderer-neutral live stream metadata with newest-value buffering, duplicate suppression, rate limiting, and Broadcastify-compatible alpha-tag updates isolated from PSI and PCM delivery
  • Renderer-neutral audio encoder process lifecycle with immutable commands, bounded shutdown, stderr diagnostics, and injectable process factories
  • Pluggable local playback with bounded newest-audio buffering, preserved PortAudio behavior, and explicit PipeWire, PulseAudio, and ALSA adapters
  • Per-subscriber PCM health snapshots, ordered transitions, lifecycle metrics, redacted errors, and isolated startup, submission, and shutdown failures
  • Renderer-neutral single-owner runtime for scanner control, PSI, one RTSP/RTP fanout, dynamic PCM destinations, immutable snapshots, and deterministic cleanup
  • Versioned local daemon API over a private Unix-domain socket with backward-compatible snapshots, capability-checked scanner controls, strict JSON Lines envelopes, bounded clients, and deterministic shutdown
  • Versioned ordered local daemon event stream over a separate private Unix socket with authoritative snapshots, bounded subscriptions, and explicit sequence-gap resynchronization
  • Explicit sdsctl daemon-client workflows for negotiated status and snapshot reads, safe typed scanner controls, validated gap-detecting event watches, and daemon-owned PCMU playback or WAV recording
  • Versioned bounded local daemon PCMU stream over a third private Unix socket with accepted RTP payloads, continuity metadata, and independent client-loss counters
  • Optional live playback through the local default or selected audio output device
  • Simultaneous local playback and streaming PCM WAV recording from one RTSP session
  • UDP XML fragment validation, statistics, and bounded retries
  • Bash and Zsh tab completion
  • Strict MyPy typing, Ruff checks, and hardware-independent tests

Network audio remains independent from scanner control, so playback and recording do not open or affect the USB serial or UDP control transport. See the project roadmap for ordered work and the project vision for broader deferred capabilities.

Requirements

  • Python 3.11 or newer
  • A Uniden SDS100, SDS150, or SDS200
  • For USB: scanner connected as a serial device
  • For Linux desktop USB access, see the optional udev rule
  • For Ethernet: scanner and computer on a trusted local network

Linux USB, Ethernet control, and RTSP/RTP audio recording have been validated with an SDS200 running firmware version 1.26.01. SDS100 USB control has also been validated on firmware 1.26.01. SDS150 support follows Uniden's shared SDS-series remote-command specification and still needs physical-hardware validation. Explicit SDS200 network hosts work on any platform supported by Python's TCP and UDP sockets. Automatic route detection and /dev/serial/by-id discovery are Linux-specific.

Installation

Install the published package from PyPI:

python -m pip install sds200

Install the optional full-screen TUI:

python -m pip install "sds200[tui]"

Install optional local audio playback support:

python -m pip install "sds200[playback]"

On Linux, the Python extra requires the operating system's PortAudio runtime. Install it on Debian or Raspberry Pi OS before starting playback:

sudo apt update
sudo apt install libportaudio2

Inspect the PortAudio build, host APIs, default output, and output-capable devices:

sdsctl audio-devices

Depending on the operating-system audio stack and PortAudio build, Linux devices may be exposed through ALSA, PipeWire compatibility, PulseAudio compatibility, or JACK. The CLI and TUI continue to use PortAudio by default. Python integrations can instead construct a BufferedPlaybackSink with PipeWirePlaybackAdapter, PulseAudioPlaybackAdapter, or AlsaPlaybackAdapter; the corresponding pw-cat, pacat, or aplay executable must be installed.

Install the TUI with live and saved-recording playback:

python -m pip install "sds200[tui,playback]"

Install from source for development:

git clone https://github.com/stevenboyd78/sds200-python.git
cd sds200-python
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip

For development:

python -m pip install -e ".[dev]"

Quick start

Find connected scanners

Search USB and directly connected IPv4 networks:

sdsctl discover

Search a specific network:

sdsctl discover --network 192.168.0.0/24 --network-only

Active LAN discovery sends the read-only MDL command to each usable host. Only scan networks you own or are authorized to probe.

USB serial

Show scanner information using automatic model detection:

sdsctl info

Select a specific model when multiple USB scanners are connected:

sdsctl --model SDS100 info
sdsctl --model SDS150 info

Start the live monitor:

sdsctl monitor

Launch the optional Textual interface:

sdsctl tui

Press Q to quit, T to switch semantic palettes, C to reconnect, G to show or hide the operational log panel, and ? for the full keyboard reference. The TUI provides non-blocking scanner controls, responsive compact, standard, and wide layouts, dense short-screen audio and PSI summaries, live and saved SDS200 audio playback, repeatable recordings, mode-aware special-screen panels, operational logging, and rate-limited stale-PSI recovery through USB, network, profile, and replay selectors. A sustained stale PSI stream is automatically reconnected without stopping active network audio; see the Textual TUI guide.

With an explicit SDS200 network host, opt in to WAV recording and use R to start or stop the one-shot recording session:

sdsctl --host 192.168.0.251 tui \
  --audio-output scanner-audio.wav

Use an explicit port when automatic discovery is not appropriate:

sdsctl \
  --port /dev/serial/by-id/usb-UNIDEN_AMERICA_CORP._SDS200_Serial_Port-if00 \
  info

SDS200 Ethernet

sdsctl --host 192.168.0.251 info
sdsctl --host 192.168.0.251 scanner-info
sdsctl --host 192.168.0.251 monitor

The SDS200 virtual serial service uses UDP port 50536 by default.

Foreground SDS200 daemon

Run one long-lived scanner-control, PSI, and network-audio ownership runtime in the foreground:

sdsctl --log-level INFO --host 192.168.0.251 daemon

A saved network or fallback SDS200 profile may supply the endpoint:

sdsctl --log-level INFO --profile home daemon

The process owns one scanner control session, one PSI stream, one SDS200 RTSP/RTP session, and one decoded-PCM router. A fallback profile may use serial control while its configured network host remains the audio endpoint.

The daemon exposes three versioned local services through private Unix-domain sockets:

  • $XDG_RUNTIME_DIR/sdsctl/daemon.sock, or the user-state fallback, provides the request-response API for authoritative snapshots and safe typed scanner controls. Select an explicit absolute path with --socket-path.
  • $XDG_RUNTIME_DIR/sdsctl/events.sock, or the user-state fallback, provides the ordered JSON Lines event stream. Select an explicit absolute path with --event-socket-path.
  • $XDG_RUNTIME_DIR/sdsctl/pcmu.sock, or the user-state fallback, provides accepted RTP PCMU packets through a bounded binary stream. Select an explicit absolute path with --pcmu-socket-path.

Use the explicit daemon client when another process owns the scanner:

sdsctl daemon-client status
sdsctl daemon-client snapshot
sdsctl daemon-client hold TGID 12345
sdsctl daemon-client next TGID 12345 --count 1
sdsctl daemon-client reconnect
sdsctl daemon-client events --count 10 --json
sdsctl daemon-client audio --play
sdsctl daemon-client audio \
  --output scanner-audio.wav \
  --duration 30
sdsctl tui --daemon-client
sdsctl tui --daemon-client \
  --audio-playback \
  --audio-directory ~/recordings

API options such as --socket-path precede the client action. Event watching and audio use their separate --event-socket-path and --pcmu-socket-path options after the corresponding action. The top-level scanner commands remain the explicit standalone workflows.

Every event client first receives an authoritative runtime snapshot at the current global sequence boundary, then only later runtime, scanner, PSI, radio-state, audio-lifecycle, and destination-health events. Sequence gaps show that an event subscriber overflowed; reconnect to obtain a new authoritative snapshot.

Every PCMU client receives only packets accepted after its independent subscription is created. Frames preserve RTP sequence, timestamp, SSRC, continuity estimates, observation time, endpoint, raw payload bytes, and cumulative loss caused by that client's bounded queue.

Stop the process with Ctrl+C or SIGTERM. Shutdown first closes API clients, then stops daemon-owned destinations, then stops scanner, PSI, audio, and router ownership, closes PCMU clients, and finally closes event clients after final lifecycle transitions. All three owned sockets are removed.

The command remains in the foreground for service-manager ownership. It does not fork, create a pidfile, install a service, expose TCP, accept unrestricted raw scanner commands, or provide decoded-PCM client subscriptions. The local API supports documented hold, next, previous, and bounded reconnect operations. Reconnect is available only when the daemon directly owns the SDS200 UDP control transport; fallback or serial control returns unsupported_operation. The complete safe-control sequence has been physically validated while API, event, PSI, RTSP/RTP, decoded-audio, and two PCMU clients remained active, followed by clean SIGTERM shutdown and socket removal. The daemon-owned audio client has also been physically validated with simultaneous default-device playback and WAV recording. It received 258 consecutive loss-free PCMU frames, finalized a 10.320-second 8 kHz mono WAV, preserved API health, and shut down with all three sockets removed. Its bounded local playback queue reported six overflows and 2,088 dropped PCM bytes without underflow; the daemon PCMU stream itself remained loss-free.

The top-level scanner commands and direct scanner-audio commands remain standalone. The TUI also remains standalone by default; add --daemon-client to explicitly use a foreground daemon. In daemon mode, the TUI reads identity and authoritative initial state from daemon.sock, follows ordered updates from events.sock, delegates safe controls through the daemon API, and consumes daemon-owned PCMU from pcmu.sock for live playback, recording, metadata, and the saved-recording library. It does not open scanner hardware or a second RTSP/RTP session. Closing the TUI closes only its local clients and leaves daemon ownership running. Reconnect requests are delegated through the daemon API. Direct TUI RTSP/RTP options are rejected in daemon mode.

This path was physically validated on August 5, 2026, with a physical SDS200. The daemon-backed TUI rendered cleanly, followed live state, completed a safe control, automatically started playback, toggled playback with A, and produced a valid 53.120-second 8 kHz mono WAV with an adjacent metadata sidecar. Quitting the TUI left scanner, PSI, audio, router, and daemon ownership healthy. A later controlled SIGTERM removed daemon.sock, events.sock, and pcmu.sock.

The daemon loads the explicit --destination-config path or ${XDG_CONFIG_HOME:-~/.config}/sdsctl/daemon-destinations.toml before opening scanner hardware. Saved playback, recording, and remote-profile destinations are activated under daemon ownership. SIGHUP transactionally reloads that exact manifest while preserving the previous committed set on failure.

Decoded-PCM subscriptions and automatic daemon selection remain follow-on work. See the daemon deployment and upgrade guide, daemon runtime and process guide, local daemon API guide, local daemon event stream guide, local daemon PCMU stream guide, and operational logging.

Serial-only profiles, replay captures, and non-SDS200 network-audio selections are rejected.

SDS200 network audio playback and recording

Listen to the scanner through the operating system's default audio output:

sdsctl --host 192.168.0.251 audio --play

Record the scanner's RTSP/RTP audio directly to an 8 kHz mono signed 16-bit PCM WAV file without requiring FFmpeg:

sdsctl --host 192.168.0.251 audio \
  --output scanner-audio.wav \
  --duration 30

Omit --duration to record until Ctrl+C. Existing files are protected unless overwrite is explicitly enabled:

sdsctl --host 192.168.0.251 audio \
  --output scanner-audio.wav \
  --force

Playback and recording can share one RTSP/RTP session:

sdsctl --host 192.168.0.251 audio \
  --play \
  --output scanner-audio.wav

Use --device DEVICE to choose a PortAudio output device and --buffer-ms to change the bounded playback queue. Playback underflow inserts silence; overflow drops the oldest queued audio so the output remains live. The summary reports both conditions and the number of dropped PCM bytes.

The scanner requires a nonstandard single RTP client port during RTSP SETUP. The built-in transport handles that negotiation, receives payload type 0 PCMU, decodes each accepted packet once, and fans the PCM out to independently buffered destinations. WAV disk writes and local sound-device callbacks cannot block RTP reception. The same long-lived fanout session powers TUI playback, repeatable recording, and saved-recording controls without opening a second scanner stream.

Each recording summary reports estimated packet loss, duplicates, late and malformed packets, and RTP timestamp discontinuities. A five-minute SDS200 hardware soak received 7,500 packets and 2,400,000 audio samples with every reliability counter remaining at zero.

Connection profiles and fallback

Create a profile directly from USB and LAN discovery:

sdsctl profile discover home \
  --network 192.168.0.0/24 \
  --prefer network

When both endpoints are found, the profile automatically falls back between Ethernet and USB. The saved preference can be overridden for one command:

sdsctl --profile home --prefer serial monitor

Preferred recovery is disabled by default. Enable validated return to the saved preferred endpoint when creating a fallback profile:

sdsctl profile discover home \
  --network 192.168.0.0/24 \
  --prefer network \
  --recover-preferred \
  --recovery-probe-interval 30 \
  --recovery-stability-window 5 \
  --recovery-cooldown 30

Override the saved recovery policy for one process:

sdsctl --profile home \
  --recover-preferred \
  --recovery-probe-interval 15 \
  monitor

Recovery opens the inactive preferred endpoint, sends MDL, verifies that an SDS200 responds, waits through the stability window, probes again, and promotes only while no request/response command is pending. Continuous PSI updates are restarted after promotion.

Manual profiles remain supported, including explicit fallback profiles:

sdsctl profile add network-only --host 192.168.0.251
sdsctl profile add usb-only \
  --port /dev/serial/by-id/usb-UNIDEN_AMERICA_CORP._SDS200_Serial_Port-if00 \
  --model SDS200
sdsctl profile add handheld --port /dev/ttyACM0 --model SDS150
sdsctl profile add home \
  --port /dev/serial/by-id/usb-UNIDEN_AMERICA_CORP._SDS200_Serial_Port-if00 \
  --host 192.168.0.251 \
  --prefer network \
  --recover-preferred

Profiles are stored in ${XDG_CONFIG_HOME:-~/.config}/sds200/profiles.toml. This legacy connection-profile document remains separate from layered application configuration, and --config PATH continues to override only this profile file.

Layered application configuration

Optional application settings are loaded from /etc/sdsctl/config.toml, ${XDG_CONFIG_HOME:-~/.config}/sdsctl/config.toml, supported SDSCTL_* environment variables, and explicit CLI options in that precedence order. Missing files preserve the existing defaults and are never created automatically.

See Layered application configuration for the versioned TOML schema, supported fields, environment-variable names, path behavior, validation rules, and Python provenance API.

Repair stale USB paths or a changed scanner IP address without losing the saved transport preference:

sdsctl profile repair home --network 192.168.0.0/24
sdsctl profile repair home --network 192.168.0.0/24 --dry-run

Reliability, health, and events

sdsctl --profile home health
sdsctl --profile home health --watch 5 --history
sdsctl --profile home health --watch 5 --history --json
sdsctl --profile home events --json
sdsctl --host 192.168.0.251 --trace scanner.trace monitor

Select an operational log level or append logs to a persistent file:

sdsctl --log-level INFO --host 192.168.0.251 monitor
sdsctl --log-level DEBUG --log-file /var/log/sdsctl.log \
  --host 192.168.0.251 events

-v selects INFO, -vv selects DEBUG, and an explicit --log-level overrides verbosity. Raw scanner traffic remains separate under --trace. See Operational logging for journald, permissions, and logrotate examples.

Reconnects use capped exponential backoff. Retry forever by default, or set a finite recovery budget:

sdsctl --profile home \
  --reconnect-attempts 8 \
  --reconnect-initial-delay 1 \
  --reconnect-multiplier 2 \
  --reconnect-max-delay 30 \
  monitor

events --json emits one JSON object per line for connection changes, transport diagnostics, reconnect scheduling, failovers, preferred recoveries, and live state changes.

Capabilities, capture, and replay

Show the connected model's limits and validation status:

sdsctl --model SDS100 capabilities

Record a replayable session and then run the same operation without hardware:

sdsctl --model SDS100 --capture sds100-info.jsonl info
sdsctl --replay sds100-info.jsonl --model SDS100 info

Captures can contain local scanner data. Use repeated --redact TEXT options and inspect files before sharing them. See Session capture and replay.

Typed navigation

The documented HLD, NXT, and PRV operations are available through typed APIs and CLI commands:

sdsctl --model SDS100 hold SYS 100
sdsctl --model SDS100 next DEPT 200 100 --count 2
sdsctl --model SDS100 previous TGID 300

Targets and indexes are protocol values reported by GSI or GLT. The typed navigation layer is specification-backed and replay-tested; smoke-test it on the intended scanner before relying on it operationally. Prefer a replay fixture when developing navigation logic without a scanner attached.

Raw protocol commands

sdsctl command MDL
sdsctl command VER
sdsctl command GCS  # SDS150 charge status
sdsctl command VOL
sdsctl command SQL
sdsctl command STS

Raw command access is intended for documented scanner commands and protocol development. Prefer the typed Python methods when they are available.

Shell completion

Activate Bash completion for the current shell:

eval "$(sdsctl completion bash)"

Enable it whenever Bash starts:

echo 'eval "$(sdsctl completion bash)"' >> ~/.bashrc

For Zsh:

eval "$(sdsctl completion zsh)"

Python API

USB

from sds200 import SDSScanner

with SDSScanner.auto(model="SDS150") as radio:
    print(radio.get_model())
    print(radio.get_firmware())
    print(radio.get_volume())
    print(radio.get_squelch())

SDS200 Ethernet

from sds200 import SDSScanner

with SDSScanner.network("192.168.0.251") as radio:
    info = radio.get_scanner_info()
    print(info.system)
    print(info.department)
    print(info.channel)
    print(info.frequency)

Continuous state updates

from sds200 import SDSScanner

with SDSScanner.network("192.168.0.251") as radio:
    radio.on_state_change(
        lambda change: print(change.fields, change.current.channel)
    )

    with radio.scanner_info_push(interval_ms=500):
        radio.wait()

Reconnect policy and health history

from sds200 import ReconnectPolicy, SDSScanner

policy = ReconnectPolicy(
    initial_delay=1.0,
    multiplier=2.0,
    max_delay=30.0,
    max_attempts=8,
)

with SDSScanner.network("192.168.0.251", reconnect_policy=policy) as radio:
    print(radio.health_check().as_dict())
    print(radio.health_summary().as_dict())

LAN discovery

from sds200 import discover_network_scanners

for scanner in discover_network_scanners(["192.168.0.0/24"]):
    print(scanner.endpoint, scanner.model, scanner.latency_ms)

Project naming

The model-neutral executable is sdsctl. New application configuration, state, and cache paths use the sdsctl namespace. The distribution, Python import package, and repository remain named sds200, while legacy profile files remain under the existing sds200 configuration root. Python applications should use SDSScanner; the historical SDS200 class name remains an alias.

Security

The SDS200 network-control protocol is unauthenticated and unencrypted. Keep it on a trusted LAN or access it through a secured VPN. Do not expose UDP port 50536 directly to the public Internet.

This library is not a safety-critical or emergency-dispatch system. Do not rely on it as the sole means of receiving urgent communications.

See SECURITY.md for vulnerability reporting and docs/transports.md for transport limitations.

Documentation

Development

python -m pip install -e ".[dev]"

ruff check .
mypy src/sds200
pytest
python scripts/check_docs.py
python -m build
python -m twine check dist/*

Tests must not require physical scanner hardware. Hardware validation is documented separately in pull requests and release notes.

Project status

Version 0.15.0 hardens the optional Textual workstation interface for sustained Raspberry Pi operation. It adds configurable operational logging, automatic rate-limited recovery from a connected-but-stale PSI stream, and deterministic fault-injection coverage for audio startup, shutdown, repeated requests, and scanner reconnects. Failed PSI reconnects preserve their requested update interval so recovery continues after control traffic returns, while active SDS200 network audio remains uninterrupted. SDS100 and SDS200 control support is hardware-validated. SDS150 support is implemented and fixture-tested, but physical validation is deferred until representative hardware is available. It does not block unrelated releases. API compatibility is not guaranteed until version 1.0.

See CHANGELOG.md for development history, ROADMAP.md for ordered work, and the project vision for deferred capabilities and architectural direction.

Acknowledgments

This project is created and maintained by Steven Boyd, with substantial development assistance from OpenAI's ChatGPT. AI-assisted contributions are reviewed and tested by the maintainer before inclusion. See ACKNOWLEDGMENTS.md.

License

MIT. See LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sds200-0.19.0.tar.gz (2.2 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sds200-0.19.0-py3-none-any.whl (271.2 kB view details)

Uploaded Python 3

File details

Details for the file sds200-0.19.0.tar.gz.

File metadata

  • Download URL: sds200-0.19.0.tar.gz
  • Upload date:
  • Size: 2.2 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for sds200-0.19.0.tar.gz
Algorithm Hash digest
SHA256 4370128015aa508896a0e9bb60176796a972718c9fe8a1f1fcb9b8375636fc00
MD5 2f341413d2eeb5150188de3796c87192
BLAKE2b-256 722cab09b34c3824aa2f8aa7b9fe7a625e6c7ff7be81b8e9f2550f8547d9e39f

See more details on using hashes here.

Provenance

The following attestation bundles were made for sds200-0.19.0.tar.gz:

Publisher: release.yml on stevenboyd78/sds200-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file sds200-0.19.0-py3-none-any.whl.

File metadata

  • Download URL: sds200-0.19.0-py3-none-any.whl
  • Upload date:
  • Size: 271.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for sds200-0.19.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a152a07353966638571e6bcc0e5db47dd1176a3d90873113c31ff4e390e6e6bc
MD5 c5d44634a95e13b4276476974bb03c09
BLAKE2b-256 8caffe6c9231adb6f27206256081776eeaf27939e72b3927e6854e0f33144acf

See more details on using hashes here.

Provenance

The following attestation bundles were made for sds200-0.19.0-py3-none-any.whl:

Publisher: release.yml on stevenboyd78/sds200-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page