WP Manifestindependent plugin directory
manifest / admin / dsn-hawk

DSN Hawk

Collects site health + configuration reports and pushes them to the DSN Skyline Laravel admin panel.

by Juan Tamayo, DesignStudio Network · github.com/designstudio-dev-team/dsn-hawk · website

0stars
2release downloads
0forks

Install

The author publishes release zips, so WP-CLI can install straight from GitHub:

wp plugin install https://github.com/designstudio-dev-team/dsn-hawk/releases/download/v1.0.2/dsn-hawk.zip

Declares an update source (https://github.com/DesignStudio-Dev-Team/dsn-hawk), so updates arrive through the plugin's own updater.

Readme

DSN Hawk

WordPress plugin that collects site health + configuration reports and pushes them to the Skyline admin panel in the DSN Laravel app.

Hawk is the agent that runs on each WordPress site. Skyline is the central Laravel dashboard that receives and displays Hawk reports. Hawk knows nothing about other sites; Skyline aggregates everything.


Status

v0.1 — in development. First feature: Gravity Forms report.


Architecture

┌─────────────────────┐         POST /api/v1/hawk/sync        ┌─────────────────────┐
│  WordPress Site     │  ──────────────────────────────────▶  │   Laravel (DSN)     │
│                     │     Bearer: WP_SYNC_TOKEN             │                     │
│  DSN Hawk plugin    │     JSON { site, reports }            │   Skyline admin     │
└─────────────────────┘                                        └─────────────────────┘
  • Hawk collects data locally on the WP site.
  • Hawk POSTs a single payload per sync run to Skyline's /api/v1/hawk/sync endpoint.
  • Skyline upserts the site by domain; first sync auto-creates the skyline_sites row.
  • Each "report" inside the payload is a namespaced slice (e.g. gravity_forms, later email_log, plugins).
  • Skyline treats missing report keys as "no update" — Hawk can push partial payloads.

Plugin Settings (WP Admin)

Settings page: Settings → DSN Hawk

Setting Description
Skyline endpoint URL e.g. https://dsn.example.com/api/v1/hawk/sync
Sync token Bearer token, matches Laravel WP_SYNC_TOKEN env var
Sync frequency daily / hourly / manual (via WP-Cron)
Enabled reports checkboxes per report type
Manual "Sync now" button triggers an immediate push, shows last response

Store in wp_options under the key dsn_hawk_settings (single serialized array).


Payload Contract

One unified endpoint, one payload shape. Hawk sends everything it has in a single POST.

{
  "site": {
    "name": "Example Site",
    "domain": "example.com",
    "url": "https://example.com",
    "admin_email": "admin@example.com",
    "wp_site_id": 1,
    "is_multisite": false,
    "timezone": "America/Los_Angeles",
    "server_ip": "192.0.2.10",
    "email_enabled": true
  },
  "reports": {
    "gravity_forms": {
      "forms": [
        {
          "id": "1",
          "title": "Contact Form",
          "is_active": true,
          "total_entries_count": 42,
          "fields": [
            { "id": "1", "label": "Email", "admin_label": "", "type": "email", "is_required": true, "visibility": "visible", "inputs": [] }
          ],
          "notifications": [
            { "id": "abc123", "name": "Admin Notification", "is_active": true, "to": "leads@example.com", "to_type": "email", "to_field": "", "bcc": "archive@example.com", "from": "{admin_email}", "reply_to": "{Email:1}", "subject": "New submission" }
          ],
          "entries": [
            {
              "id": "100",
              "entry_id": "100",
              "form_id": "1",
              "date_created": "2026-05-04 18:00:00",
              "date_updated": "2026-05-04 18:00:00",
              "status": "active",
              "source_url": "https://example.com/contact",
              "user_agent": "Mozilla/5.0...",
              "ip": "203.0.113.0",
              "created_by": null,
              "is_starred": false,
              "is_read": false,
              "fields": [
                { "id": "1", "field_id": "1", "label": "Email", "type": "email", "value": "sha256:...", "value_redacted": true, "value_truncated": false, "value_original_length": 71 },
                { "id": "2", "field_id": "2", "label": "Name", "type": "name", "value": "[redacted]", "value_redacted": true, "value_truncated": false, "value_original_length": 10 }
              ],
              "field_values": { "1": "sha256:...", "2": "[redacted]" }
            }
          ],
          "entries_meta": {
            "mode": "incremental",
            "cursor_before": 99,
            "cursor_after": 100,
            "backfilled": true,
            "batch_size": 25,
            "per_sync_budget": 25,
            "field_value_max_length": 180,
            "returned": 1,
            "pii_stripped": true
          }
        }
      ]
    },
    "plugins": {
      "last_update_check": 1777932000,
      "last_update_check_at": "2026-05-04T18:00:00+00:00",
      "plugins": [
        { "file": "gravityforms/gravityforms.php", "slug": "gravityforms", "name": "Gravity Forms", "version": "2.9.0", "latest_version": "2.9.1", "is_active": true, "auto_update": false, "update_available": true, "update_status": "update_available", "last_modified": "2026-04-20T12:00:00+00:00", "last_updated_at": "2026-04-20T12:00:00+00:00" }
      ]
    },
    "core_theme": {
      "wordpress": {
        "version": "6.8.1",
        "latest": "6.8.1",
        "update_available": false,
        "update_status": "current",
        "last_update_check": 1777932000,
        "last_update_check_at": "2026-05-04T18:00:00+00:00",
        "last_successful_update_version": "6.8.1",
        "last_updated_at": "2026-04-30T16:30:00+00:00"
      }
    }
  }
}

site fields

Field Source
name get_bloginfo( 'name' )
domain parse_url(home_url(), PHP_URL_HOST) — Skyline normalizes (strips protocol, www., path)
url home_url()
admin_email get_bloginfo( 'admin_email' )
wp_site_id get_current_blog_id(); useful for multisite installs
is_multisite is_multisite()
timezone wp_timezone_string()
server_ip $_SERVER['SERVER_ADDR'] (fallback: gethostbyname($host))
email_enabled Boolean. True if WP can send mail — see Email sending detection below

Response

  • 200 { "status": "ok" } — success
  • 401 { "status": "unauthorized" } — bad bearer token
  • 422 — validation error (log + retry on next cycle)

Report: Gravity Forms (v0.1)

Goal: show every form on the site, whether it's active, where notifications go, and whether the site can email.

Collection

Only runs if the Gravity Forms plugin is active. Check with:

if ( ! class_exists( 'GFAPI' ) ) {
    return null; // skip this report
}

For each form returned by GFAPI::get_forms( true /* active */ ) and GFAPI::get_forms( false /* inactive */ ):

Payload field Source
id $form['id'] (cast to string)
title $form['title']
description $form['description']
is_active ! $form['is_trash'] && $form['is_active']
is_trash $form['is_trash']
date_created, date_updated Gravity Forms form metadata when available
total_entries_count GFAPI::count_entries( $form['id'] )
fields[] Form field schema: id, label, admin_label, type, is_required, visibility, inputs[]
notifications[] Notification metadata: id, name, is_active, to, to_type, to_field, bcc, from, reply_to, subject
entries[] Real Gravity Forms entries only. Each entry includes id, entry_id, form_id, date_created, date_updated, status/read/star metadata, normalized labeled fields[], and field_values keyed by GF field/input ID.
entries_meta Cursor and batching metadata so Skyline can tell whether the site is still backfilling

Privacy mode keeps entry metadata (id, entry_id, form_id, date_created, status, read/star flags, masked IP, truncated user agent, source URL without query string) but strips or masks PII field values. Email values are hashed as sha256:...; name, phone, address, website, and likely PII labels are sent as [redacted] with value_redacted: true. Hawk does not create placeholder N/A rows; if a batch has no real entries, entries is an empty array.

Skyline should not turn redacted values into N/A. Display [redacted] when value_redacted is true, show blank/unknown only for actual null metadata, and never create synthetic entry rows when entries is empty.

Each outbound field value is capped to 180 characters by default to avoid receiver database truncation errors. If Hawk truncates a value, it sends value_truncated: true and value_original_length. Store the capped value in short columns like field_data; use the flags to indicate that the original submission had more data.

Backfill starts with cursor 0, sends historical entries ordered by real Gravity Forms entry ID, and only advances each form cursor after Skyline returns a 2xx response. Hawk limits each sync run to a total entry budget of 25 entries across all forms by default, so the initial push is spread across multiple syncs instead of posting every form's first entries at once. Forms that did not get entry budget in a run report entries_meta.mode: "deferred" and will continue on a later sync. Once a short batch or empty batch confirms history is drained, Hawk marks the form backfilled and subsequent syncs continue with cursor-based new entries (id > cursor).

Developers can tune entry batching with filters:

  • dsn_hawk_gf_batch_size: maximum entries pulled for one form at a time, default 25.
  • dsn_hawk_gf_entries_per_sync: maximum total entries included in one sync payload across all forms, default 25.
  • dsn_hawk_gf_field_value_max_length: maximum characters per field value, default 180.

Notification extraction

Gravity Forms stores notifications as an associative array keyed by notification ID. Each has to, bcc, toType (email | field | routing). For email type, to is a literal address; for field, it's a field ID reference — send the raw value, Skyline will display as-is.

Email sending detection (site.email_enabled)

Heuristic (fast, no outbound test email):

  1. true if any of these is present: WP Mail SMTP plugin active w/ configured mailer, Post SMTP, FluentSMTP, SendGrid official plugin, Mailgun plugin — and their respective "test connection" status is OK.
  2. true if wp_mail() is not short-circuited and phpmailer_init has a non-default Host.
  3. false if an SMTP plugin is active but reports "not configured" / last test failed.
  4. null (unknown) otherwise.

Cache the result in a transient for 1 hour to avoid re-running on every sync.


File Structure

dsn-hawk/
├── dsn-hawk.php              # Main plugin file (headers + bootstrap)
├── README.md
├── composer.json             # PSR-4 autoload for src/
├── src/
│   ├── Plugin.php            # bootstrap, hooks
│   ├── Settings/
│   │   └── SettingsPage.php  # admin settings UI
│   ├── Sync/
│   │   ├── Syncer.php        # builds + POSTs payload
│   │   ├── Scheduler.php     # WP-Cron registration
│   │   └── HttpClient.php    # wp_remote_post wrapper + retry
│   ├── Reports/
│   │   ├── ReportInterface.php
│   │   ├── GravityFormsReport.php
│   │   ├── EmailLogReport.php    # (future)
│   │   └── PluginsReport.php     # (future)
│   └── Support/
│       └── SiteInfo.php      # domain, IP, email-sending check
└── assets/
    └── admin.css

Report interface

Each report is self-contained:

interface ReportInterface {
    public function key(): string;            // e.g. "gravity_forms"
    public function isAvailable(): bool;      // e.g. Gravity Forms plugin active?
    public function collect(): ?array;        // payload slice, or null to skip
}

The Syncer iterates enabled reports, calls collect() on each, and assembles the final reports object.


WP-Cron

Register a custom interval on activation:

add_filter( 'cron_schedules', function ( $s ) {
    $s['dsn_hawk_hourly'] = [ 'interval' => HOUR_IN_SECONDS, 'display' => 'DSN Hawk — Hourly' ];
    return $s;
} );

register_activation_hook( __FILE__, function () {
    if ( ! wp_next_scheduled( 'dsn_hawk_sync' ) ) {
        wp_schedule_event( time() + 60, 'dsn_hawk_hourly', 'dsn_hawk_sync' );
    }
} );

register_deactivation_hook( __FILE__, function () {
    wp_clear_scheduled_hook( 'dsn_hawk_sync' );
} );

add_action( 'dsn_hawk_sync', [ Syncer::class, 'run' ] );

On high-traffic sites, recommend hooking to a real system cron hitting wp-cron.php rather than relying on WP's request-triggered cron.


HTTP Client

Use wp_remote_post(). Important knobs:

wp_remote_post( $endpoint, [
    'timeout'     => 15,
    'blocking'    => true,
    'headers'     => [
        'Authorization' => 'Bearer ' . $token,
        'Content-Type'  => 'application/json',
        'Accept'        => 'application/json',
        'User-Agent'    => 'DSN-Hawk/' . DSN_HAWK_VERSION,
    ],
    'body'        => wp_json_encode( $payload ),
    'data_format' => 'body',
] );

Retry strategy:

  • On 5xx or WP_Error: store the payload in a transient, retry on next scheduled run.
  • On 401: disable sync, surface "Invalid token" notice in WP admin.
  • On 422: log to the plugin's own log table (or error_log), don't retry — fix the payload first.

Logging

Own table: {prefix}dsn_hawk_log — columns id, created_at, status, http_code, message, payload_bytes. Keep last 100 runs for debugging. Shown on the settings page.


Read the full README on GitHub →

Releases

TagPublishedAssetDownloads
v1.0.2 May 12, 2026 dsn-hawk.zip 1
v1.0.1 May 12, 2026 dsn-hawk.zip 1
v1.0.0 May 12, 2026 dsn-hawk.zip 0