TL;DR at a glance
What this PR does: Extends the existing weekly usage payload sent to aiousage.com/v1/track with ~160 additive top-level fields — installation context, environment, feature toggles, content aggregates, AI / notification / writing-assistant / SEO-analyzer / GSC / redirect / revision counts, addon matrix, and WooCommerce — on Pro installs only. Lite payload is bit-for-bit identical to develop. Endpoint unchanged. Cadence unchanged (weekly, Action Scheduler). Opt-in unchanged (existing usageTracking toggle for Lite, default-on for Pro).
Privacy contract rules of the road
What we DO collect
- Counts (e.g. redirects_total, posts_with_focus_keyphrase_count)
- Timestamps (e.g. last_post_published_at)
- Booleans (e.g. gsc_site_verified, feature_breadcrumbs_enabled)
- Enum values and enum distributions ({type: count})
- Arrays of slug strings (which features have non-empty config)
- Schema-type names, error codes, HTTP codes, internal status enums
What we NEVER collect
- Post titles, descriptions, content, slugs, or user-authored URLs
- Focus keyphrase text (count only)
- Schema body / template body
- Business addresses, phone numbers, geo coordinates
- API keys, OAuth tokens (existing redaction preserved)
- Client IPs, user-agents, referrers
- Per-redirect source/target URLs
- Per-keyword text (Search Statistics, Writing Assistant)
- Derived scores, ratios, or computed maturity tiers
Decisions log since initial PR draft
Phase 1 inventory ~180 telemetry data points · expand each category
∅ Pre-existing fields (already shipping today, unchanged)
These are the fields the plugin already sends to aiousage.com/v1/track and have been live for years. They stay exactly as-is — the PR is purely additive on top of them.
| Field | Type | Applies | Source / notes |
|---|---|---|---|
| url | string | Both | home_url() — kept; receiver-side joins depend on this |
| string | Both | Site admin email | |
| php_version | string | Both | PHP runtime version |
| wp_version | string | Both | WordPress core version |
| mysql_version | string | Both | MySQL / MariaDB version |
| server_version | string | Both | Web server (e.g. Apache, nginx) |
| is_ssl | bool | Both | HTTPS in use |
| is_multisite | bool | Both | Network install |
| sites_count | int | Both | Multisite subsite count |
| user_count | int | Both | Total WordPress users |
| active_plugins | object | Both | Plugin basename → version map |
| theme_name | string | Both | Active theme |
| theme_version | string | Both | Active theme version |
| locale | string | Both | WP locale (e.g. en_US) |
| timezone_offset | string | Both | UTC offset |
| aioseo_version | string | Both | AIOSEO plugin version |
| aioseo_license_key | string | Pro | Existing Pro field — unchanged |
| aioseo_license_type | string | Pro | Tier name (Essential, Plus, Pro, Elite, Agency, Lifetime) |
| aioseo_is_pro | bool | Pro | Always true for Pro payloads |
| aioseo_lite_installed_date | timestamp | Both | When Lite was first installed |
| aioseo_pro_installed_date | timestamp | Pro | When Pro was first installed |
| aioseo_settings | object | Both | Full options + internalOptions blob (kept for receiver-side decoding) |
| addon_data | object | Pro | Per-addon contributions via addon Usage::getData (most empty today) |
1 Installation context
Stable identifiers for joining payloads over time and seeing where the install came from. UUID and install_first_seen_at were removed this pass per the "no opaque identifiers" rule; email-hash replaces UUID as the warehouse-side join key.
| # | Field | Type | Applies | Source / notes |
|---|---|---|---|---|
| 1.1 | install_uuid Removed | string (v4) | Both | Removed 2026-05-25 — "no opaque identifiers" rule |
| 1.2 | install_first_seen_at Removed | timestamp | Both | Removed — was UUID-coupled; redundant with aioseo_*_installed_date |
| 1.3 | wizard_completed_step | int | Both | Furthest setup-wizard step reached (0 = never started) |
| 1.4 | wizard_category | enum | Both | blog / business / ecommerce / news / other |
| 1.5 | wizard_category_other_present | bool | Both | True if user filled "Other" free-text (text itself never sent) |
| 1.6 | aioseo_last_active_version | string | Both | Previously active version (useful for upgrade-path tracking) |
| 1.7 | aioseo_pro_first_activated_at | timestamp | Pro | First-ever Pro activation |
| 1.8 | aioseo_pro_activated_at | timestamp | Pro | Most recent Pro activation |
| + | admin_email_hash New | string | Both | Server-side hashing on receive (plugin sends plaintext email as today; warehouse hashes for joins) |
2 Environment
Server / WordPress runtime parameters that explain support-pain root causes.
| # | Field | Type | Applies | Source / notes |
|---|---|---|---|---|
| 2.1 | php_memory_limit | string | Both | ini_get('memory_limit') e.g. "256M" |
| 2.2 | php_max_execution_time | int | Both | Seconds |
| 2.3 | php_post_max_size | string | Both | e.g. "8M" |
| 2.4 | wp_memory_limit | string | Both | WP_MEMORY_LIMIT constant |
| 2.5 | wp_max_upload_size | int | Both | Bytes |
| 2.6 | wp_debug | bool | Both | WP_DEBUG constant |
| 2.7 | wp_cron_disabled | bool | Both | DISABLE_WP_CRON constant |
| 2.8 | is_block_theme | bool | Both | Full Site Editing theme in use |
| 2.9 | db_collation | string | Both | e.g. utf8mb4_unicode_ci |
3 Site configuration
Site shape: permalink structure, what kinds of content exist, who can register, whether search engines are allowed.
| # | Field | Type | Applies | Source / notes |
|---|---|---|---|---|
| 3.1 | permalink_structure | string | Both | Pattern only (e.g. /%postname%/), never a URL |
| 3.2 | public_post_type_count | int | Both | Number of public post types registered |
| 3.3 | public_post_type_slugs | array<string> | Both | Slug names only (e.g. post, page, product) |
| 3.4 | public_taxonomy_slugs | array<string> | Both | Slug names only (e.g. category, post_tag) |
| 3.5 | home_url_matches_site_url | bool | Both | Detects WP-in-subdir installs |
| 3.6 | users_can_register | bool | Both | Open registration enabled |
| 3.7 | blog_public | bool | Both | "Discourage search engines" inverted |
| 3.8 | default_comment_status_open | bool | Both | Comments open by default on new posts |
4 Feature enablement (toggles)
One boolean per major AIOSEO feature surface. Lets product see adoption of each module independently of aioseo_settings blob decoding. (Two candidate toggles — searchStatistics.enable, writingAssistant.enable — were dropped during impl because no such option exists; usage is captured via the row-count fields in §11 and §13 instead.)
| # | Field | Type | Applies | Source / notes |
|---|---|---|---|---|
| 4.1 | feature_sitemap_general_enabled | bool | Both | XML sitemap on |
| 4.2 | feature_sitemap_rss_enabled | bool | Both | RSS sitemap on |
| 4.3 | feature_sitemap_html_enabled | bool | Both | HTML sitemap on |
| 4.4 | feature_sitemap_llms_enabled | bool | Both | LLMS.txt sitemap (AI crawler hints) |
| 4.5 | feature_sitemap_advanced_enabled | bool | Both | Advanced sitemap settings expanded |
| 4.6 | feature_breadcrumbs_enabled | bool | Both | Breadcrumbs module on |
| 4.7 | feature_seo_analysis_enabled | bool | Both | Site-wide SEO analysis tool on |
| 4.8 | feature_email_summary_enabled | bool | Both | Weekly email summary on |
| 4.9 | feature_usage_tracking_enabled | bool | Both | Telemetry opt-in (true by definition when Lite payloads arrive) |
| 4.10 | feature_facebook_og_enabled | bool | Both | Open Graph tags enabled |
| 4.11 | feature_facebook_advanced_enabled | bool | Both | Advanced Facebook settings on |
| 4.12 | feature_twitter_card_enabled | bool | Both | Twitter/X cards enabled |
| 4.13 | feature_social_same_username_enabled | bool | Both | Single-username toggle for social profiles |
| 4.14 | feature_crawl_cleanup_enabled | bool | Both | Crawl cleanup feature on |
| 4.17 | feature_ai_manually_connected | bool | Both | User manually connected AI service |
| 4.18 | feature_ai_trial_token | bool | Both | Using the free AI trial token (vs paid credits) |
5 Feature adoption (counts)
"On" alone isn't enough. These counts tell you whether a feature is being actively populated with config — social profiles, sitemap exclusions, notification queue depth.
| # | Field | Type | Applies | Source / notes |
|---|---|---|---|---|
| 5.1 | social_profile_urls_set_count | int | Both | How many social profile URLs are filled in (URLs never sent) |
| 5.2 | social_profile_urls_set_keys | array<string> | Both | Which platforms are filled (facebookPageUrl, twitter, instagram…) |
| 5.3 | sitemap_excluded_posts_count | int | Both | User-excluded posts in sitemap |
| 5.4 | sitemap_excluded_terms_count | int | Both | User-excluded terms in sitemap |
| 5.5 | sitemap_additional_pages_count | int | Both | Manually added external URLs in sitemap |
| 5.6 | sitemap_general_links_per_index | int | Both | User-configured chunk size |
| 5.7 | notifications_total_count | int | Both | Lifetime notifications queued for the install |
| 5.8 | notifications_dismissed_count | int | Both | User-dismissed |
| 5.9 | notifications_unread_count | int | Both | Currently unread |
| 5.10 | notifications_active_count | int | Both | Active right now (within start/end window) |
| 5.11 | notifications_by_type | object | Both | {type_slug: count} — type slugs are AM-owned strings |
| 5.12 | crawl_cleanup_logs_count | int | Both | Logged crawl-cleanup blocks |
| 5.13 | crawl_cleanup_blocked_args_count | int | Both | User-configured blocked query args |
| 5.14 | seo_checklist_completed_count | int | Both | Items completed in the SEO checklist |
| 5.15 | deprecated_options_in_use_count | int | Both | How many deprecated options the install still uses |
| 5.16 | deprecated_options_in_use_keys | array<string> | Both | Which deprecated keys (internal names only) |
6 Usage frequency (timestamps)
"When was X last done" signals — snapshot-safe, derivable from existing columns. No new event-collection infrastructure needed.
| # | Field | Type | Applies | Source / notes |
|---|---|---|---|---|
| 6.1 | last_post_published_at | timestamp | Both | Most recent publish across all post types |
| 6.2 | last_post_modified_at | timestamp | Both | Most recent edit |
| 6.3 | posts_published_30d_count | int | Both | Publishing velocity (30-day rolling) |
| 6.4 | posts_modified_30d_count | int | Both | Editing velocity |
| 6.5 | aioseo_post_last_updated_at | timestamp | Both | Last AIOSEO metadata save |
| 6.6 | aioseo_post_last_image_scan_at | timestamp | Both | Last image-SEO scan |
| 6.7 | aioseo_post_last_video_scan_at | timestamp | Both | Last video sitemap scan |
| 6.8 | aioseo_post_last_seo_analyzer_scan_at | timestamp | Both | Last TruSEO scan |
| 6.9 | notification_last_created_at | timestamp | Both | Most recent notification queued |
7 Content optimization (post-level aggregates)
How thoroughly is the user actually optimizing content — custom titles, descriptions, OG images, canonicals, robots meta — plus a four-band histogram of TruSEO scores. Never includes the text of any field — only counts.
| # | Field | Type | Applies | Source / notes |
|---|---|---|---|---|
| 7.1 | aioseo_posts_row_count | int | Both | Total rows in the AIOSEO posts table |
| 7.2 | posts_with_focus_keyphrase_count | int | Both | Posts with at least one focus keyphrase set (text never sent) |
| 7.3 | posts_with_custom_title_count | int | Both | Posts overriding the default title template |
| 7.4 | posts_with_custom_description_count | int | Both | Posts with custom meta description |
| 7.5 | posts_with_canonical_url_count | int | Both | Custom canonical URLs set (URLs never sent) |
| 7.6 | posts_with_og_image_count | int | Both | Custom OG images set |
| 7.7 | posts_with_twitter_image_count | int | Both | Custom Twitter card images set |
| 7.8 | posts_robots_noindex_count | int | Both | Posts marked noindex |
| 7.9 | posts_robots_nofollow_count | int | Both | Posts marked nofollow |
| 7.10 | posts_robots_noarchive_count | int | Both | Posts marked noarchive |
| 7.11 | posts_pillar_content_count | int | Both | Posts marked as pillar/cornerstone content |
| 7.12 | posts_limit_modified_date_count | int | Both | Posts opted into the "don't surface modified date" feature |
| 7.13 | posts_seo_score_band_none_count | int | Both | Score = 0 (never analyzed) |
| 7.14 | posts_seo_score_band_red_count | int | Both | Score 1–49 |
| 7.15 | posts_seo_score_band_orange_count | int | Both | Score 50–74 |
| 7.16 | posts_seo_score_band_green_count | int | Both | Score 75+ (Adam's note: histogram bands of an existing UI bucket, not a new derived score) |
8 Schema usage
Which schema graphs the site is actually emitting. Schema bodies are never sent — only the type-name enum and a count of how many posts have manual schema graphs added.
| # | Field | Type | Applies | Source / notes |
|---|---|---|---|---|
| 8.1 | posts_schema_type_distribution | object | Both | {Article: 312, Product: 47, ...} — type names only |
| 8.2 | posts_with_custom_schema_count | int | Both | Posts with any manual schema graphs (bodies never sent) |
| 8.3 | schema_templates_count | int | Pro | Number of saved schema templates |
9 Term-level configuration Pro
Taxonomy-term-level SEO config is Pro-only. Mirrors the post-level aggregates in §7.
| # | Field | Type | Applies | Source / notes |
|---|---|---|---|---|
| 9.1 | aioseo_terms_row_count | int | Pro | Total rows in AIOSEO terms table |
| 9.2 | terms_with_custom_title_count | int | Pro | Terms with custom title |
| 9.3 | terms_with_custom_description_count | int | Pro | Terms with custom description |
| 9.4 | terms_with_og_image_count | int | Pro | Terms with custom OG image |
| 9.5 | terms_robots_noindex_count | int | Pro | Terms marked noindex |
| 9.6 | terms_pillar_content_count | int | Pro | Terms marked as pillar content |
10 AI feature usage
AI credit balance + AI Insights report counts. We emit _total and _remaining separately, never _used — consumer derives any ratio it needs.
| # | Field | Type | Applies | Source / notes |
|---|---|---|---|---|
| 10.1 | ai_credits_total | int | Both | Total AI credits granted |
| 10.2 | ai_credits_remaining | int | Both | AI credits left |
| 10.3 | ai_license_credits_total | int | Both | License-grant AI credit pool total |
| 10.4 | ai_license_credits_remaining | int | Both | License-grant AI credits left |
| 10.5 | ai_license_credits_expires_at | timestamp | Both | When license AI quota expires |
| 10.6 | ai_credit_orders_count | int | Both | How many credit purchases (order IDs never sent) |
| 10.7 | ai_cost_per_feature | object | Both | {feature_slug: credits_spent} — slugs only |
| 10.8 | posts_with_ai_data_count | int | Both | Posts with any AI-generated content stored (text never sent) |
| 10.9 | ai_insights_reports_count | int | Both | Total AI Insights keyword reports run |
| 10.10 | ai_insights_reports_by_status | object | Both | {status: count} — pending/running/complete/failed |
| 10.11 | ai_insights_last_report_at | timestamp | Both | Most recent AI Insights report |
11 Writing Assistant
How much the Writing Assistant feature is used. Keyword text is never sent — only counts and country/language enum distributions.
| # | Field | Type | Applies | Source / notes |
|---|---|---|---|---|
| 11.1 | writing_assistant_posts_count | int | Both | Posts using Writing Assistant (body never sent) |
| 11.2 | writing_assistant_keywords_count | int | Both | Keywords researched (text never sent) |
| 11.3 | writing_assistant_keywords_country_distribution | object | Both | {2-letter country: count} |
| 11.4 | writing_assistant_keywords_language_distribution | object | Both | {language code: count} |
| 11.5 | writing_assistant_last_updated_at | timestamp | Both | Most recent Writing Assistant activity |
12 SEO Analyzer
Counts of analyzer runs, competitor analyses, and what check codes triggered. Competitor URLs themselves are never sent.
| # | Field | Type | Applies | Source / notes |
|---|---|---|---|---|
| 12.1 | seo_analyzer_results_count | int | Both | Total analyzer runs stored |
| 12.2 | seo_analyzer_competitor_results_count | int | Both | Competitor analyses (URL never sent) |
| 12.3 | seo_analyzer_last_run_at | timestamp | Both | Most recent analyzer run |
| 12.4 | seo_analyzer_objects_count | int | Pro | Total findings (Pro) |
| 12.5 | seo_analyzer_objects_ignored_count | int | Pro | User-ignored findings |
| 12.6 | seo_analyzer_objects_code_distribution | object | Pro | {check_code: count} — internal codes |
13 Search Statistics / GSC Pro
Whether GSC is connected, sitemap inspection counts, indexing/coverage verdict distributions, tracked-keyword counts. No keyword text and no per-URL inspection data.
| # | Field | Type | Applies | Source / notes |
|---|---|---|---|---|
| 13.1 | gsc_site_verified | bool | Pro | GSC site verification successful |
| 13.2 | gsc_last_fetch_at | timestamp | Pro | Most recent successful GSC sync |
| 13.3 | gsc_sitemap_count_known | int | Pro | Sitemaps GSC reports back |
| 13.4 | gsc_sitemap_count_ignored | int | Pro | Sitemaps user told us to ignore |
| 13.5 | gsc_sitemap_last_fetch_at | timestamp | Pro | Most recent sitemap-status fetch |
| 13.6 | gsc_rolling_window | enum | Pro | User config: last7Days/last28Days/last3Months/etc. |
| 13.7 | gsc_objects_count | int | Pro | Total URLs we have GSC data on |
| 13.8 | gsc_objects_verdict_distribution | object | Pro | {verdict: count} |
| 13.9 | gsc_objects_indexing_state_distribution | object | Pro | {indexing_state: count} |
| 13.10 | gsc_objects_coverage_state_distribution | object | Pro | {coverage_state: count} |
| 13.11 | gsc_objects_last_inspection_at | timestamp | Pro | Most recent URL inspection |
| 13.12 | gsc_tracked_keywords_count | int | Pro | Tracked keyword count (text never sent) |
| 13.13 | gsc_favorited_keywords_count | int | Pro | User-favorited keywords |
| 13.14 | gsc_keyword_groups_count | int | Pro | User-organized keyword groups |
14 Redirects Pro
Redirect rule volume, types, hits, and 404-log counts. Source URL, target URL, agent, referrer, and IP are NEVER sent — only aggregate counts and HTTP-code distributions.
| # | Field | Type | Applies | Source / notes |
|---|---|---|---|---|
| 14.1 | redirects_total | int | Pro | Total redirect rules |
| 14.2 | redirects_enabled_count | int | Pro | Active rules |
| 14.3 | redirects_regex_count | int | Pro | Regex rules |
| 14.4 | redirects_with_post_id_count | int | Pro | Auto-generated rules tied to a specific post |
| 14.5 | redirects_type_distribution | object | Pro | {301: n, 302: n, 307: n, 308: n, 410: n, 451: n} |
| 14.6 | redirects_group_distribution | object | Pro | {group_slug: count} — AM-owned slugs |
| 14.7 | redirects_last_created_at | timestamp | Pro | Most recent redirect added |
| 14.8 | redirects_last_updated_at | timestamp | Pro | Most recent edit |
| 14.9 | redirects_total_hits | int | Pro | Sum of redirect hit counts (no per-URL breakdown) |
| 14.10 | redirects_404_total | int | Pro | Unique 404 URLs the user knows about |
| 14.11 | redirects_404_logs_30d_count | int | Pro | 404 hits in the last 30 days |
| 14.12 | redirects_404_logs_total | int | Pro | All-time 404 hit log rows |
| 14.13 | redirects_logs_total | int | Pro | All-time redirect-hit log rows |
15 SEO Revisions Pro
| # | Field | Type | Applies | Source / notes |
|---|---|---|---|---|
| 15.1 | revisions_total | int | Pro | Total SEO revision rows stored |
| 15.2 | revisions_by_object_type | object | Pro | {object_type: count} |
| 15.3 | revisions_distinct_author_count | int | Pro | Count of distinct editor IDs (IDs never sent) |
| 15.4 | revisions_last_created_at | timestamp | Pro | Most recent revision |
16 License flags Pro
Eight raw booleans + an expiry timestamp. We deliberately don't ship a derived "license_state = active/expired/invalid" enum — the warehouse composes those from these raw flags.
| # | Field | Type | Applies | Source / notes |
|---|---|---|---|---|
| 16.1 | license_expires_at | timestamp | Pro | License expiry date |
| 16.2 | license_expired | bool | Pro | Currently expired |
| 16.3 | license_invalid | bool | Pro | Validation failed |
| 16.4 | license_disabled | bool | Pro | License disabled (refund / chargeback) |
| 16.5 | license_domain_disabled | bool | Pro | Specific domain deactivated |
| 16.6 | license_connection_error | bool | Pro | Could not reach license server |
| 16.7 | license_activations_error | bool | Pro | Activation slot exceeded |
| 16.8 | license_request_error | bool | Pro | Last license-API call errored |
17 Addon install / version matrix
For each AIOSEO addon (link-assistant, local-business, video-sitemap, news-sitemap, image-seo, index-now, broken-link-checker, eeat, duplicate-post, rest-api, backlink-tracker, aioseo-redirects), we emit a tiny block describing install/active/version status. This does NOT require any addon code changes — addon-side primitive contributions (link counts, BLC results, location count, etc.) are deferred to a follow-up PR.
| # | Field | Type | Applies | Source / notes |
|---|---|---|---|---|
| 17.1 | addons[sku].installed | bool | Both | Addon zip is on disk |
| 17.2 | addons[sku].active | bool | Both | Addon is activated |
| 17.3 | addons[sku].installed_version | string | Both | Version currently on disk |
| 17.4 | addons[sku].minimum_version | string | Both | Minimum required by core |
| 17.5 | addons[sku].has_minimum_version | bool | Both | Installed meets minimum |
| 17.6 | addons[sku].requires_upgrade | bool | Both | Compatibility upgrade needed |
SKUs included: aioseo-link-assistant, aioseo-local-business, aioseo-video-sitemap, aioseo-news-sitemap, aioseo-image-seo, aioseo-index-now, aioseo-broken-link-checker, aioseo-eeat, aioseo-duplicate-post, aioseo-rest-api, aioseo-backlink-tracker, aioseo-redirects.
19 WooCommerce integration
Hoisted to top-level when WooCommerce is detected — saves the warehouse from decoding active_plugins for every payload.
| # | Field | Type | Applies | Source / notes |
|---|---|---|---|---|
| 19.1 | woocommerce_active | bool | Both | Woo plugin detected |
| 19.2 | woocommerce_version | string | Both | Hoisted from active_plugins (only when Woo active) |
| 19.3 | woocommerce_product_count_published | int | Both | Published product count |
Phase 2+ scope deferred — not in this PR, separate sign-off when ready
Cheap snapshot additions
Net-new primitives that don't need new infra. Suggested as one small follow-up PR.
- Object-cache backend enum (Redis/Memcached/WP-Rocket/none)
- Cron mode enum (real-cron/disable/AS)
- Update channel bool (auto-updates enabled)
- Site-wide schema type (Org/Person/WebSite/none)
- GSC auth state enum + PageSpeed bool
- users_by_role distribution
- Multilingual language count, EDD product count
- Multisite override counts
Admin UX + upsell funnel
Requires a tiny new counter mechanism (wp_options key incremented on Vue routes and key surfaces). Highest product value of any phase.
- Admin page-view counts per Vue route
- Setup wizard step entered / completed / skipped
- Bulk editor, Tools, metabox open counts
- Notification shown / clicked / dismissed
- Upsell modal opens + upgrade button clicks
- OAuth initiated / completed / abandoned
- License entry attempts
DB-error wrapper
Tiered $wpdb error capture with dedupe by (errno, query fingerprint, context). No query values, no table data — just errno + fingerprint hash.
- Tier 1: schema bugs (1146 table-missing, 1054 unknown-column, 1064 syntax)
- Tier 2: dbDelta mismatches (1050 already-exists, 1060 duplicate-column)
- Tier 3: concurrency (1213 deadlock, 2006 server-gone-away)
- Tier 4: rate-limited duplicate-entry / encoding issues
Per-addon primitives + p50/p95
Each addon adds primitives to its own addon_data entry. Plus performance-timing wrappers around heavy paths.
- Link Assistant: links / suggestions / scan timestamps
- BLC: broken links by status + last scan
- Local Business: location count + maps key present (bool only)
- Sitemap / TruSEO / AI / GSC p50 / p95 duration
Sign-off
Once you've reviewed each category above, check each statement below, fill in your name and date, and click Generate sign-off. Copy the resulting block into the PR conversation as your approval.