5 Commits

Author SHA1 Message Date
73ba7fb929 Add Prometheus metrics integration (v0.7.4)
All checks were successful
Create Release Package / build-release (push) Successful in 1m8s
- New Metrics settings tab with enable/disable toggle
- PrometheusController for wp_prometheus_collect_metrics hook
- License gauges: total by status, lifetime, expiring, expiring soon
- Download gauges: total downloads, active versions
- API counters: requests, rate limits, validation errors
- Metric tracking in RestApiController and UpdateController

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 10:52:50 +01:00
548b2ae8af Bump version to 0.7.3
All checks were successful
Create Release Package / build-release (push) Successful in 1m15s
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 13:53:37 +01:00
e0001c3f4e Fix API Verification Secret not visible in Docker environments
- Add ResponseSigner::getServerSecret() to check multiple sources
- Check constant, getenv(), $_ENV, and $_SERVER for server secret
- Update Plugin.php to use ResponseSigner::isSigningEnabled()
- Maintains backward compatibility with standard WordPress setups

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 13:52:57 +01:00
a879be989c Update CLAUDE.md with Docker environment variable fix session
- Documented bug fix for API Verification Secret not visible in Docker
- Added ResponseSigner::getServerSecret() method documentation
- Removed known bug from roadmap (now fixed)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 13:51:57 +01:00
40c08bf474 Update CLAUDE.md with v0.7.2 session learnings
- Document CI/CD workflow fix for handling existing releases
- Add lessons learned about Gitea releases and tag updates
- Note about not creating zip archives locally (RAM issue)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 22:44:08 +01:00
12 changed files with 3825 additions and 3360 deletions

View File

@@ -7,6 +7,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.7.4] - 2026-02-03
### Added
- **Prometheus Metrics Integration**: Expose license and API metrics for monitoring
- New "Metrics" settings tab with enable/disable toggle
- License gauges: total by status, lifetime, expiring, expiring soon
- Download gauges: total downloads, active versions count
- API counters: requests by endpoint/result, rate limit exceeded events, validation errors by type
- Requires [WP Prometheus](https://src.bundespruefstelle.ch/magdev/wp-prometheus) plugin
### New Files
- `src/Metrics/PrometheusController.php` - Prometheus metrics collection and registration
### Technical Details
- Hooks into `wp_prometheus_collect_metrics` action for metric collection
- API counters stored persistently in WordPress options (`wclp_prometheus_counters`)
- Static methods for incrementing counters from API controllers
- Metrics only collected when enabled in settings
## [0.7.3] - 2026-02-01
### Fixed
- **Docker Environment Support:** API Verification Secret now visible on customer licenses page in Docker environments
- Added `ResponseSigner::getServerSecret()` method to check multiple sources for server secret
- Checks PHP constant, `getenv()`, `$_ENV`, and `$_SERVER` in priority order
- Maintains full backward compatibility with standard WordPress installations
### Changed
- Updated `Plugin.php` to use `ResponseSigner::isSigningEnabled()` instead of direct constant check
### Technical Details
- Root cause: Docker WordPress setups using `wp-config-docker.php` with `getenv_docker()` don't always define PHP constants
- The environment variable was accessible but the constant wasn't being created
- New `getServerSecret()` method centralizes all server secret retrieval logic
## [0.7.2] - 2026-01-29 ## [0.7.2] - 2026-01-29
### Added ### Added

View File

@@ -32,7 +32,9 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
**Note for AI Assistants:** Clean this section after the specific features are done or new releases are made. Effective changes are tracked in `CHANGELOG.md`. Do not add completed versions here - document them in the Session History section at the end of this file. **Note for AI Assistants:** Clean this section after the specific features are done or new releases are made. Effective changes are tracked in `CHANGELOG.md`. Do not add completed versions here - document them in the Session History section at the end of this file.
No pending roadmap items. ### Known Bugs
None currently tracked.
## Technical Stack ## Technical Stack
@@ -1945,3 +1947,46 @@ composer install
- Automatically created by Gitea Actions CI/CD pipeline - Automatically created by Gitea Actions CI/CD pipeline
- Release package: 881 KiB with SHA256 checksum - Release package: 881 KiB with SHA256 checksum
- First automated release - all future releases will use this workflow - First automated release - all future releases will use this workflow
**Additional fixes (same session):**
- Updated README.md with Auto-Updates section and Development section
- Fixed CI/CD workflow to handle existing releases (delete before recreate)
- When updating a tag, the workflow now checks for existing releases and deletes them first
**Lessons learned:**
- Gitea releases persist even when their tag is deleted - must delete release via API
- Composer `symlink: false` doesn't always work - CI must manually replace symlinks with `cp -r`
- Never create zip archives locally on this machine (fills up RAM indefinitely)
- Gitea API endpoint for releases by tag: `GET /api/v1/repos/{owner}/{repo}/releases/tags/{tag}`
### 2026-02-01 - Bug Fix: API Verification Secret Not Visible
**Overview:**
Fixed the "API Verification Secret" (customer secret) not appearing on the customer account licenses page in Docker environments.
**Root Cause:**
The `WC_LICENSE_SERVER_SECRET` constant was not being defined even though the environment variable was set. In Docker WordPress setups using `wp-config-docker.php`, the `getenv_docker()` function retrieves values from environment variables, but the constant wasn't being created properly. The plugin was only checking for the PHP constant, not the environment variable directly.
**Fix:**
Added `ResponseSigner::getServerSecret()` static method that checks multiple sources for the server secret:
1. `WC_LICENSE_SERVER_SECRET` constant (standard WordPress configuration)
2. `getenv('WC_LICENSE_SERVER_SECRET')` (Docker environments)
3. `$_ENV['WC_LICENSE_SERVER_SECRET']` (some PHP configurations)
4. `$_SERVER['WC_LICENSE_SERVER_SECRET']` (fallback)
**Modified files:**
- `src/Api/ResponseSigner.php` - Added `getServerSecret()` method, updated `isSigningEnabled()` and `getCustomerSecretForLicense()` to use it
- `src/Plugin.php` - Updated to use `ResponseSigner::isSigningEnabled()` instead of direct constant check
**Technical notes:**
- The fix maintains backward compatibility with standard WordPress installations using constants
- Docker environments can now use environment variables directly without needing the constant to be defined
- All three methods (`isSigningEnabled()`, `getCustomerSecretForLicense()`, and constructor) now use the centralized `getServerSecret()` method

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -65,6 +65,7 @@ final class SettingsController
'auto-updates' => __('Auto-Updates', 'wc-licensed-product'), 'auto-updates' => __('Auto-Updates', 'wc-licensed-product'),
'defaults' => __('Default Settings', 'wc-licensed-product'), 'defaults' => __('Default Settings', 'wc-licensed-product'),
'notifications' => __('Notifications', 'wc-licensed-product'), 'notifications' => __('Notifications', 'wc-licensed-product'),
'metrics' => __('Metrics', 'wc-licensed-product'),
]; ];
} }
@@ -116,6 +117,7 @@ final class SettingsController
'auto-updates' => $this->getAutoUpdatesSettings(), 'auto-updates' => $this->getAutoUpdatesSettings(),
'defaults' => $this->getDefaultsSettings(), 'defaults' => $this->getDefaultsSettings(),
'notifications' => $this->getNotificationsSettings(), 'notifications' => $this->getNotificationsSettings(),
'metrics' => $this->getMetricsSettings(),
default => $this->getPluginLicenseSettings(), default => $this->getPluginLicenseSettings(),
}; };
} }
@@ -314,6 +316,32 @@ final class SettingsController
]; ];
} }
/**
* Get metrics settings
*/
private function getMetricsSettings(): array
{
return [
'metrics_section_title' => [
'name' => __('Prometheus Metrics', 'wc-licensed-product'),
'type' => 'title',
'desc' => __('Expose license and API metrics for Prometheus monitoring. Requires the WP Prometheus plugin to be installed and active.', 'wc-licensed-product'),
'id' => 'wc_licensed_product_section_metrics',
],
'metrics_enabled' => [
'name' => __('Enable Prometheus Metrics', 'wc-licensed-product'),
'type' => 'checkbox',
'desc' => __('Expose license statistics, API usage, and download metrics via Prometheus.', 'wc-licensed-product'),
'id' => 'wc_licensed_product_metrics_enabled',
'default' => 'no',
],
'metrics_section_end' => [
'type' => 'sectionend',
'id' => 'wc_licensed_product_section_metrics_end',
],
];
}
/** /**
* Render settings tab content * Render settings tab content
*/ */
@@ -575,4 +603,12 @@ final class SettingsController
wp_send_json_error(['message' => $error]); wp_send_json_error(['message' => $error]);
} }
} }
/**
* Check if Prometheus metrics are enabled
*/
public static function isMetricsEnabled(): bool
{
return get_option('wc_licensed_product_metrics_enabled', 'no') === 'yes';
}
} }

View File

@@ -26,9 +26,7 @@ final class ResponseSigner
public function __construct() public function __construct()
{ {
$this->serverSecret = defined('WC_LICENSE_SERVER_SECRET') $this->serverSecret = self::getServerSecret();
? WC_LICENSE_SERVER_SECRET
: '';
} }
/** /**
@@ -185,7 +183,7 @@ final class ResponseSigner
*/ */
public static function getCustomerSecretForLicense(string $licenseKey): ?string public static function getCustomerSecretForLicense(string $licenseKey): ?string
{ {
$serverSecret = defined('WC_LICENSE_SERVER_SECRET') ? WC_LICENSE_SERVER_SECRET : ''; $serverSecret = self::getServerSecret();
if (empty($serverSecret)) { if (empty($serverSecret)) {
return null; return null;
@@ -201,6 +199,40 @@ final class ResponseSigner
*/ */
public static function isSigningEnabled(): bool public static function isSigningEnabled(): bool
{ {
return defined('WC_LICENSE_SERVER_SECRET') && !empty(WC_LICENSE_SERVER_SECRET); return !empty(self::getServerSecret());
}
/**
* Get the server secret from constant or environment variable
*
* Checks in order:
* 1. WC_LICENSE_SERVER_SECRET constant (preferred)
* 2. WC_LICENSE_SERVER_SECRET environment variable (Docker fallback)
*
* @return string The server secret, or empty string if not configured
*/
public static function getServerSecret(): string
{
// First check the constant (standard WordPress configuration)
if (defined('WC_LICENSE_SERVER_SECRET') && !empty(WC_LICENSE_SERVER_SECRET)) {
return WC_LICENSE_SERVER_SECRET;
}
// Fallback to environment variable (Docker environments)
$envSecret = getenv('WC_LICENSE_SERVER_SECRET');
if ($envSecret !== false && !empty($envSecret)) {
return $envSecret;
}
// Also check $_ENV and $_SERVER (some PHP configurations)
if (!empty($_ENV['WC_LICENSE_SERVER_SECRET'])) {
return $_ENV['WC_LICENSE_SERVER_SECRET'];
}
if (!empty($_SERVER['WC_LICENSE_SERVER_SECRET'])) {
return $_SERVER['WC_LICENSE_SERVER_SECRET'];
}
return '';
} }
} }

View File

@@ -10,6 +10,7 @@ declare(strict_types=1);
namespace Jeremias\WcLicensedProduct\Api; namespace Jeremias\WcLicensedProduct\Api;
use Jeremias\WcLicensedProduct\License\LicenseManager; use Jeremias\WcLicensedProduct\License\LicenseManager;
use Jeremias\WcLicensedProduct\Metrics\PrometheusController;
use WP_REST_Request; use WP_REST_Request;
use WP_REST_Response; use WP_REST_Response;
use WP_REST_Server; use WP_REST_Server;
@@ -108,6 +109,10 @@ final class RestApiController
'retry_after' => $retryAfter, 'retry_after' => $retryAfter,
], 429); ], 429);
$response->header('Retry-After', (string) $retryAfter); $response->header('Retry-After', (string) $retryAfter);
// Track rate limit event for metrics
PrometheusController::incrementRateLimitExceeded('api');
return $response; return $response;
} }
@@ -209,6 +214,16 @@ final class RestApiController
$statusCode = $this->getStatusCodeForResult($result); $statusCode = $this->getStatusCodeForResult($result);
// Track metrics
if ($result['valid']) {
PrometheusController::incrementApiRequest('validate', 'success');
} else {
PrometheusController::incrementApiRequest('validate', 'error');
if (!empty($result['error'])) {
PrometheusController::incrementValidationError($result['error']);
}
}
return new WP_REST_Response($result, $statusCode); return new WP_REST_Response($result, $statusCode);
} }
@@ -247,6 +262,9 @@ final class RestApiController
$license = $this->licenseManager->getLicenseByKey($licenseKey); $license = $this->licenseManager->getLicenseByKey($licenseKey);
if (!$license) { if (!$license) {
PrometheusController::incrementApiRequest('status', 'error');
PrometheusController::incrementValidationError('license_not_found');
return new WP_REST_Response([ return new WP_REST_Response([
'valid' => false, 'valid' => false,
'error' => 'license_not_found', 'error' => 'license_not_found',
@@ -254,6 +272,8 @@ final class RestApiController
], 404); ], 404);
} }
PrometheusController::incrementApiRequest('status', 'success');
return new WP_REST_Response([ return new WP_REST_Response([
'valid' => $license->isValid(), 'valid' => $license->isValid(),
'status' => $license->getStatus(), 'status' => $license->getStatus(),
@@ -280,6 +300,9 @@ final class RestApiController
$license = $this->licenseManager->getLicenseByKey($licenseKey); $license = $this->licenseManager->getLicenseByKey($licenseKey);
if (!$license) { if (!$license) {
PrometheusController::incrementApiRequest('activate', 'error');
PrometheusController::incrementValidationError('license_not_found');
return new WP_REST_Response([ return new WP_REST_Response([
'success' => false, 'success' => false,
'error' => 'license_not_found', 'error' => 'license_not_found',
@@ -288,6 +311,9 @@ final class RestApiController
} }
if (!$license->isValid()) { if (!$license->isValid()) {
PrometheusController::incrementApiRequest('activate', 'error');
PrometheusController::incrementValidationError('license_invalid');
return new WP_REST_Response([ return new WP_REST_Response([
'success' => false, 'success' => false,
'error' => 'license_invalid', 'error' => 'license_invalid',
@@ -299,6 +325,8 @@ final class RestApiController
// Check if already activated on this domain // Check if already activated on this domain
if ($license->getDomain() === $normalizedDomain) { if ($license->getDomain() === $normalizedDomain) {
PrometheusController::incrementApiRequest('activate', 'success');
return new WP_REST_Response([ return new WP_REST_Response([
'success' => true, 'success' => true,
'message' => __('License is already activated for this domain.', 'wc-licensed-product'), 'message' => __('License is already activated for this domain.', 'wc-licensed-product'),
@@ -307,6 +335,9 @@ final class RestApiController
// Check if can activate on another domain // Check if can activate on another domain
if (!$license->canActivate()) { if (!$license->canActivate()) {
PrometheusController::incrementApiRequest('activate', 'error');
PrometheusController::incrementValidationError('max_activations_reached');
return new WP_REST_Response([ return new WP_REST_Response([
'success' => false, 'success' => false,
'error' => 'max_activations_reached', 'error' => 'max_activations_reached',
@@ -318,6 +349,9 @@ final class RestApiController
$success = $this->licenseManager->updateLicenseDomain($license->getId(), $domain); $success = $this->licenseManager->updateLicenseDomain($license->getId(), $domain);
if (!$success) { if (!$success) {
PrometheusController::incrementApiRequest('activate', 'error');
PrometheusController::incrementValidationError('activation_failed');
return new WP_REST_Response([ return new WP_REST_Response([
'success' => false, 'success' => false,
'error' => 'activation_failed', 'error' => 'activation_failed',
@@ -325,6 +359,8 @@ final class RestApiController
], 500); ], 500);
} }
PrometheusController::incrementApiRequest('activate', 'success');
return new WP_REST_Response([ return new WP_REST_Response([
'success' => true, 'success' => true,
'message' => __('License activated successfully.', 'wc-licensed-product'), 'message' => __('License activated successfully.', 'wc-licensed-product'),

View File

@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Jeremias\WcLicensedProduct\Api; namespace Jeremias\WcLicensedProduct\Api;
use Jeremias\WcLicensedProduct\License\LicenseManager; use Jeremias\WcLicensedProduct\License\LicenseManager;
use Jeremias\WcLicensedProduct\Metrics\PrometheusController;
use Jeremias\WcLicensedProduct\Product\VersionManager; use Jeremias\WcLicensedProduct\Product\VersionManager;
use Jeremias\WcLicensedProduct\Product\ProductVersion; use Jeremias\WcLicensedProduct\Product\ProductVersion;
use WP_REST_Request; use WP_REST_Request;
@@ -113,6 +114,10 @@ final class UpdateController
'retry_after' => $retryAfter, 'retry_after' => $retryAfter,
], 429); ], 429);
$response->header('Retry-After', (string) $retryAfter); $response->header('Retry-After', (string) $retryAfter);
// Track rate limit event for metrics
PrometheusController::incrementRateLimitExceeded('update-check');
return $response; return $response;
} }
@@ -179,10 +184,14 @@ final class UpdateController
$validationResult = $this->licenseManager->validateLicense($licenseKey, $domain); $validationResult = $this->licenseManager->validateLicense($licenseKey, $domain);
if (!$validationResult['valid']) { if (!$validationResult['valid']) {
$errorType = $validationResult['error'] ?? 'license_invalid';
PrometheusController::incrementApiRequest('update-check', 'error');
PrometheusController::incrementValidationError($errorType);
return new WP_REST_Response([ return new WP_REST_Response([
'success' => false, 'success' => false,
'update_available' => false, 'update_available' => false,
'error' => $validationResult['error'] ?? 'license_invalid', 'error' => $errorType,
'message' => $validationResult['message'] ?? __('License validation failed.', 'wc-licensed-product'), 'message' => $validationResult['message'] ?? __('License validation failed.', 'wc-licensed-product'),
], $validationResult['error'] === 'license_not_found' ? 404 : 403); ], $validationResult['error'] === 'license_not_found' ? 404 : 403);
} }
@@ -190,6 +199,9 @@ final class UpdateController
// Get license to access product ID // Get license to access product ID
$license = $this->licenseManager->getLicenseByKey($licenseKey); $license = $this->licenseManager->getLicenseByKey($licenseKey);
if (!$license) { if (!$license) {
PrometheusController::incrementApiRequest('update-check', 'error');
PrometheusController::incrementValidationError('license_not_found');
return new WP_REST_Response([ return new WP_REST_Response([
'success' => false, 'success' => false,
'update_available' => false, 'update_available' => false,
@@ -202,6 +214,9 @@ final class UpdateController
$product = wc_get_product($productId); $product = wc_get_product($productId);
if (!$product) { if (!$product) {
PrometheusController::incrementApiRequest('update-check', 'error');
PrometheusController::incrementValidationError('product_not_found');
return new WP_REST_Response([ return new WP_REST_Response([
'success' => false, 'success' => false,
'update_available' => false, 'update_available' => false,
@@ -214,6 +229,8 @@ final class UpdateController
$latestVersion = $this->getLatestVersionForLicense($license); $latestVersion = $this->getLatestVersionForLicense($license);
if (!$latestVersion) { if (!$latestVersion) {
PrometheusController::incrementApiRequest('update-check', 'success');
return new WP_REST_Response([ return new WP_REST_Response([
'success' => true, 'success' => true,
'update_available' => false, 'update_available' => false,
@@ -230,6 +247,8 @@ final class UpdateController
// Build response // Build response
$response = $this->buildUpdateResponse($product, $latestVersion, $license, $updateAvailable); $response = $this->buildUpdateResponse($product, $latestVersion, $license, $updateAvailable);
PrometheusController::incrementApiRequest('update-check', 'success');
return new WP_REST_Response($response); return new WP_REST_Response($response);
} }

View File

@@ -0,0 +1,259 @@
<?php
/**
* Prometheus Metrics Controller
*
* @package Jeremias\WcLicensedProduct\Metrics
*/
declare(strict_types=1);
namespace Jeremias\WcLicensedProduct\Metrics;
use Jeremias\WcLicensedProduct\Admin\SettingsController;
use Jeremias\WcLicensedProduct\License\LicenseManager;
use Jeremias\WcLicensedProduct\Product\VersionManager;
/**
* Exposes license and API metrics for Prometheus monitoring
*/
final class PrometheusController
{
/**
* Option name for storing API counters
*/
private const COUNTERS_OPTION = 'wclp_prometheus_counters';
private LicenseManager $licenseManager;
private VersionManager $versionManager;
public function __construct(LicenseManager $licenseManager, VersionManager $versionManager)
{
$this->licenseManager = $licenseManager;
$this->versionManager = $versionManager;
}
/**
* Register hooks for Prometheus metrics collection
*/
public function register(): void
{
// Only register if metrics are enabled
if (!SettingsController::isMetricsEnabled()) {
return;
}
add_action('wp_prometheus_collect_metrics', [$this, 'collectMetrics']);
}
/**
* Collect and register all metrics
*
* @param object $collector The Prometheus collector object
*/
public function collectMetrics(object $collector): void
{
$this->collectLicenseMetrics($collector);
$this->collectDownloadMetrics($collector);
$this->collectApiMetrics($collector);
}
/**
* Collect license-related metrics
*/
private function collectLicenseMetrics(object $collector): void
{
$stats = $this->licenseManager->getStatistics();
// License count by status (gauge)
$licensesByStatus = $collector->register_gauge(
'wclp_licenses_total',
'Total number of licenses by status',
['status']
);
foreach ($stats['by_status'] as $status => $count) {
$licensesByStatus->set($count, [$status]);
}
// Lifetime licenses (gauge)
$lifetimeLicenses = $collector->register_gauge(
'wclp_licenses_lifetime_total',
'Total number of lifetime licenses'
);
$lifetimeLicenses->set($stats['lifetime']);
// Expiring licenses (gauge)
$expiringLicenses = $collector->register_gauge(
'wclp_licenses_expiring_total',
'Total number of licenses with expiration date'
);
$expiringLicenses->set($stats['expiring']);
// Licenses expiring soon - next 30 days (gauge)
$expiringSoon = $collector->register_gauge(
'wclp_licenses_expiring_soon',
'Licenses expiring within 30 days'
);
$expiringSoon->set($stats['expiring_soon']);
}
/**
* Collect download-related metrics
*/
private function collectDownloadMetrics(object $collector): void
{
$stats = $this->versionManager->getDownloadStatistics();
// Total downloads (gauge)
$totalDownloads = $collector->register_gauge(
'wclp_downloads_total',
'Total number of file downloads'
);
$totalDownloads->set($stats['total']);
// Active versions count (gauge)
$activeVersions = $collector->register_gauge(
'wclp_versions_active_total',
'Total number of active product versions'
);
$activeVersions->set($this->countActiveVersions());
}
/**
* Collect API-related metrics (counters)
*/
private function collectApiMetrics(object $collector): void
{
$counters = $this->getCounters();
// API requests by endpoint and result (counter)
$apiRequests = $collector->register_counter(
'wclp_api_requests_total',
'Total API requests by endpoint and result',
['endpoint', 'result']
);
foreach ($counters['api_requests'] ?? [] as $key => $count) {
[$endpoint, $result] = explode(':', $key);
$apiRequests->incBy($count, [$endpoint, $result]);
}
// Rate limit exceeded events (counter)
$rateLimitExceeded = $collector->register_counter(
'wclp_rate_limit_exceeded_total',
'Total rate limit exceeded events by endpoint',
['endpoint']
);
foreach ($counters['rate_limit'] ?? [] as $endpoint => $count) {
$rateLimitExceeded->incBy($count, [$endpoint]);
}
// Validation errors by type (counter)
$validationErrors = $collector->register_counter(
'wclp_validation_errors_total',
'Total validation errors by error type',
['error_type']
);
foreach ($counters['validation_errors'] ?? [] as $errorType => $count) {
$validationErrors->incBy($count, [$errorType]);
}
}
/**
* Count active product versions
*/
private function countActiveVersions(): int
{
global $wpdb;
$tableName = \Jeremias\WcLicensedProduct\Installer::getVersionsTable();
return (int) $wpdb->get_var(
"SELECT COUNT(*) FROM {$tableName} WHERE is_active = 1"
);
}
/**
* Get stored counters
*/
private function getCounters(): array
{
$counters = get_option(self::COUNTERS_OPTION, []);
return is_array($counters) ? $counters : [];
}
/**
* Increment an API request counter
*
* @param string $endpoint The API endpoint (validate, status, activate, update-check)
* @param string $result The result (success or error)
*/
public static function incrementApiRequest(string $endpoint, string $result): void
{
if (!SettingsController::isMetricsEnabled()) {
return;
}
$counters = get_option(self::COUNTERS_OPTION, []);
if (!is_array($counters)) {
$counters = [];
}
$key = "{$endpoint}:{$result}";
$counters['api_requests'][$key] = ($counters['api_requests'][$key] ?? 0) + 1;
update_option(self::COUNTERS_OPTION, $counters, false);
}
/**
* Increment rate limit exceeded counter
*
* @param string $endpoint The API endpoint
*/
public static function incrementRateLimitExceeded(string $endpoint): void
{
if (!SettingsController::isMetricsEnabled()) {
return;
}
$counters = get_option(self::COUNTERS_OPTION, []);
if (!is_array($counters)) {
$counters = [];
}
$counters['rate_limit'][$endpoint] = ($counters['rate_limit'][$endpoint] ?? 0) + 1;
update_option(self::COUNTERS_OPTION, $counters, false);
}
/**
* Increment validation error counter
*
* @param string $errorType The error type (license_not_found, domain_mismatch, etc.)
*/
public static function incrementValidationError(string $errorType): void
{
if (!SettingsController::isMetricsEnabled()) {
return;
}
$counters = get_option(self::COUNTERS_OPTION, []);
if (!is_array($counters)) {
$counters = [];
}
$counters['validation_errors'][$errorType] = ($counters['validation_errors'][$errorType] ?? 0) + 1;
update_option(self::COUNTERS_OPTION, $counters, false);
}
/**
* Reset all counters (useful for testing or maintenance)
*/
public static function resetCounters(): void
{
delete_option(self::COUNTERS_OPTION);
}
}

View File

@@ -26,6 +26,7 @@ use Jeremias\WcLicensedProduct\Frontend\AccountController;
use Jeremias\WcLicensedProduct\Frontend\DownloadController; use Jeremias\WcLicensedProduct\Frontend\DownloadController;
use Jeremias\WcLicensedProduct\License\LicenseManager; use Jeremias\WcLicensedProduct\License\LicenseManager;
use Jeremias\WcLicensedProduct\License\PluginLicenseChecker; use Jeremias\WcLicensedProduct\License\PluginLicenseChecker;
use Jeremias\WcLicensedProduct\Metrics\PrometheusController;
use Jeremias\WcLicensedProduct\Product\LicensedProductType; use Jeremias\WcLicensedProduct\Product\LicensedProductType;
use Jeremias\WcLicensedProduct\Product\VersionManager; use Jeremias\WcLicensedProduct\Product\VersionManager;
use Jeremias\WcLicensedProduct\Update\PluginUpdateChecker; use Jeremias\WcLicensedProduct\Update\PluginUpdateChecker;
@@ -147,7 +148,7 @@ final class Plugin
new LicenseEmailController($this->licenseManager); new LicenseEmailController($this->licenseManager);
// Initialize response signing if server secret is configured // Initialize response signing if server secret is configured
if (defined('WC_LICENSE_SERVER_SECRET') && WC_LICENSE_SERVER_SECRET !== '') { if (ResponseSigner::isSigningEnabled()) {
(new ResponseSigner())->register(); (new ResponseSigner())->register();
} }
@@ -171,6 +172,9 @@ final class Plugin
if (!empty($serverUrl) && !$licenseChecker->isSelfLicensing()) { if (!empty($serverUrl) && !$licenseChecker->isSelfLicensing()) {
PluginUpdateChecker::getInstance()->register(); PluginUpdateChecker::getInstance()->register();
} }
// Initialize Prometheus metrics if enabled
(new PrometheusController($this->licenseManager, $this->versionManager))->register();
} }
/** /**

View File

@@ -3,7 +3,7 @@
* Plugin Name: WooCommerce Licensed Product * Plugin Name: WooCommerce Licensed Product
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-licensed-product * Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-licensed-product
* Description: WooCommerce plugin to sell software products using license keys with domain-based validation. * Description: WooCommerce plugin to sell software products using license keys with domain-based validation.
* Version: 0.7.2 * Version: 0.7.4
* Author: Marco Graetsch * Author: Marco Graetsch
* Author URI: https://src.bundespruefstelle.ch/magdev * Author URI: https://src.bundespruefstelle.ch/magdev
* License: GPL-2.0-or-later * License: GPL-2.0-or-later
@@ -28,7 +28,7 @@ if (!defined('ABSPATH')) {
} }
// Plugin constants // Plugin constants
define('WC_LICENSED_PRODUCT_VERSION', '0.7.2'); define('WC_LICENSED_PRODUCT_VERSION', '0.7.4');
define('WC_LICENSED_PRODUCT_PLUGIN_FILE', __FILE__); define('WC_LICENSED_PRODUCT_PLUGIN_FILE', __FILE__);
define('WC_LICENSED_PRODUCT_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('WC_LICENSED_PRODUCT_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('WC_LICENSED_PRODUCT_PLUGIN_URL', plugin_dir_url(__FILE__)); define('WC_LICENSED_PRODUCT_PLUGIN_URL', plugin_dir_url(__FILE__));