Откријте промени во технолошкиот стек на клиентите: известувања во реално време за PHP 8.3
Јавниот технолошки стек на клиентот може тивко да се промени: миграција на CMS, замена на CDN, нов производ за аналитика или надградба на рамка што влијае на вашето следно распоредување. Рачната проверка на технолошки извештаи не може да се скалира, но мала закажана PHP програма може да воспостави доверлива почетна состојба и да ве известува само кога ќе се промени нешто значајно.
Овој туторијал ја гради таа програма во Native PHP 8.3. Таа го повикува API-то Website Technology Detector, го валидира одговорот на границата на апликацијата, складира по една снимка за секоја веб-страница и создава известувања погодни за cron. Доверливоста и доказите остануваат достапни за истрага, додека вообичаените флуктуации во доказите не создаваат бучни известувања.
Добијте пристап и копирајте токен со опсег на услугата
- Регистрирајте се на https://ai.mihajlo.mk/register, или користете https://ai.mihajlo.mk/login ако веќе имате сметка.
- Отворете ја страницата на услугата Website Technology Detector.
- Изберете достапен Free, Plus или Pro план и завршете ја неговата активација.
- Отворете ја официјалната документација за услугата.
- Најдете го панелот Service token и копирајте го неговиот токен со опсег на услугата.
Оваа услуга бара автентикација. Таа прифаќа Bearer токен, заглавие X-API-Token или параметар за барање token. Проектот користи Bearer токен за акредитивот да не се појавува во URL-адреси, дневници за пристап на прокси и историја на прелистувачот.
Повторното генерирање на токенот за услугата го поништува претходно активниот токен. Третирајте ја ротацијата како промена во распоредувањето: ажурирајте ја тајната насекаде каде што работи мониторот, потврдете го новиот токен и дури потоа сметајте ја ротацијата за завршена.
Потврдете ја крајната точка пред да пишувате PHP
Точното барање е POST https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies, со JSON тело што содржи url. Направете едно минимално барање од доверлив терминал:
export WEBSITE_TECH_TOKEN='YOUR_SERVICE_TOKEN'
curl --fail-with-body \
--connect-timeout 3 \
--max-time 15 \
-X POST \
'https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies' \
-H "Authorization: Bearer ${WEBSITE_TECH_TOKEN}" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
--data '{"url":"https://client.example"}'
Никогаш не ја зачувувајте таа историја на школка или вистински токен во commit. За локален развој, ставете резервирани вредности во .env.local и исклучете ја датотеката од Git:
# .env.local
WEBSITE_TECH_TOKEN=YOUR_SERVICE_TOKEN
MONITORED_URLS=https://client.example,https://shop.client.example
TECH_STATE_DIR=/var/lib/stack-monitor
# .gitignore
.env.local
.phpunit.cache/
vendor/
Native PHP не вчитува датотеки со променливи на околината автоматски. При локална проверка, извезете ја оваа датотека преку вашата школка со set -a; . ./.env.local; set +a. Во продукција, внесете ги истите променливи преку закажувачот, механизмот за тајни на контејнерот или управувачот со услуги.
Изберете мала архитектура свесна за неуспеси
Закажана команда е подобра од веб-рута. Откривањето е работа во заднина, ниту еден корисник не треба да чека за неа, а cron веќе обезбедува закажување и испорака на е-пошта на обични Linux хостови.
Патеката на податоците е намерно кратка:
- Посветен API клиент го испраќа автентираното барање со ограничени временски рокови и повторни обиди.
- Маперот ги валидира откривањата, доверливоста, доказите, верзиите и информациите за пренасочување.
- Командата ги споредува стабилните идентитети на технологиите, верзиите и пренасочувањата со зачуваната снимка.
- Атомско ажурирање на состојбата станува следната почетна состојба.
- Командата печати само промени или неуспеси, овозможувајќи му на cron да го извести развивачот.
Првото успешно извршување создава почетна состојба без да подигне известување. Доверливоста и доказите се зачувуваат за дијагноза, но се исклучени од откривањето на промени бидејќи малите варијации во доказите инаку може да создадат повторливи известувања.
Создадете Native PHP проект
На проектот му се потребни PHP 8.3, екстензиите cURL и JSON, Composer и PHPUnit за автоматизирани тестови.
{
"name": "example/client-stack-monitor",
"require": {
"php": "^8.3",
"ext-curl": "*",
"ext-json": "*"
},
"require-dev": {
"phpunit/phpunit": "^11.0"
},
"autoload": {
"classmap": ["src/"]
},
"scripts": {
"test": "phpunit tests"
}
}
client-stack-monitor/
├── bin/
│ └── check-stack.php
├── src/
│ ├── Transport.php
│ ├── CurlTransport.php
│ ├── TechnologyDetectorClient.php
│ └── DetectionSnapshot.php
├── tests/
│ └── TechnologyDetectorClientTest.php
├── composer.json
└── .env.local
Извршете composer install откако ќе ги создадете датотеките.
Изградете HTTP граница што може да се инјектира
Апстракцијата на транспортот ги држи деталите за cURL надвор од доменот и ги прави повторните обиди тестабилни без мрежен пристап.
<?php
// src/Transport.php
declare(strict_types=1);
namespace App;
interface Transport
{
public function post(
string $url,
array $headers,
string $body,
int $connectTimeout,
int $responseTimeout
): HttpResponse;
}
final readonly class HttpResponse
{
public function __construct(
public int $status,
public string $body,
public array $headers = []
) {}
}
final class TransportFailure extends \RuntimeException {}
final class ApiFailure extends \RuntimeException
{
public function __construct(
public readonly string $kind,
string $message,
public readonly ?int $status = null
) {
parent::__construct($message);
}
}
<?php
// src/CurlTransport.php
declare(strict_types=1);
namespace App;
final class CurlTransport implements Transport
{
public function post(
string $url,
array $headers,
string $body,
int $connectTimeout,
int $responseTimeout
): HttpResponse {
$handle = curl_init($url);
if ($handle === false) {
throw new TransportFailure('Unable to initialize cURL.');
}
$responseHeaders = [];
curl_setopt_array($handle, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => $connectTimeout,
CURLOPT_TIMEOUT => $responseTimeout,
CURLOPT_HEADERFUNCTION => static function ($curl, string $line) use (&$responseHeaders): int {
$parts = explode(':', $line, 2);
if (count($parts) === 2) {
$responseHeaders[strtolower(trim($parts[0]))] = trim($parts[1]);
}
return strlen($line);
},
]);
$bodyResult = curl_exec($handle);
if ($bodyResult === false) {
$message = curl_error($handle);
curl_close($handle);
throw new TransportFailure($message ?: 'Network request failed.');
}
$status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
curl_close($handle);
return new HttpResponse($status, $bodyResult, $responseHeaders);
}
}
Додајте ограничени повторни обиди и структурирани неуспеси
Привремените мрежни грешки, ограничувањата на стапката и избраните неуспеси на серверот добиваат најмногу три обиди. Неуспесите на автентикацијата и валидацијата се враќаат веднаш: повторното обидување со невалиден токен или тело троши квота и ја одложува дијагнозата.
<?php
// src/TechnologyDetectorClient.php
declare(strict_types=1);
namespace App;
final class TechnologyDetectorClient
{
private const ENDPOINT =
'https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies';
public function __construct(
private readonly Transport $transport,
private readonly string $token,
private readonly \Closure $sleep
) {
if ($token === '') {
throw new \InvalidArgumentException('WEBSITE_TECH_TOKEN is required.');
}
}
public function detect(string $url): array
{
if (filter_var($url, FILTER_VALIDATE_URL) === false) {
throw new \InvalidArgumentException('A valid absolute URL is required.');
}
$body = json_encode(['url' => $url], JSON_THROW_ON_ERROR);
$retryable = [429, 500, 502, 503, 504];
for ($attempt = 1; $attempt <= 3; $attempt++) {
try {
$response = $this->transport->post(
self::ENDPOINT,
[
'Authorization: Bearer ' . $this->token,
'Accept: application/json',
'Content-Type: application/json',
],
$body,
3,
15
);
} catch (TransportFailure $error) {
if ($attempt === 3) {
throw new ApiFailure('network', $error->getMessage());
}
($this->sleep)(200_000 * $attempt);
continue;
}
if ($response->status >= 200 && $response->status < 300) {
try {
$decoded = json_decode($response->body, true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException $error) {
throw new ApiFailure('protocol', 'API returned invalid JSON.');
}
if (!is_array($decoded)) {
throw new ApiFailure('protocol', 'API response must be a JSON object.');
}
return $decoded;
}
if (in_array($response->status, $retryable, true) && $attempt < 3) {
$retryAfter = $response->headers['retry-after'] ?? null;
$seconds = ctype_digit((string) $retryAfter)
? min(2, (int) $retryAfter)
: 0;
($this->sleep)(
$seconds > 0 ? $seconds * 1_000_000 : 200_000 * $attempt
);
continue;
}
$kind = match ($response->status) {
400, 422 => 'validation',
401, 403 => 'authentication',
429 => 'rate_limit',
default => $response->status >= 500 ? 'service' : 'http',
};
throw new ApiFailure(
$kind,
"Technology API failed with HTTP {$response->status}.",
$response->status
);
}
throw new ApiFailure('internal', 'Retry loop ended unexpectedly.');
}
}
Мапирајте го одговорот на границата
Надворешниот JSON не смее непроверен да влезе во логиката за споредување. Маперот подолу отфрла идентитети што недостасуваат и погрешно типизирани податоци за доверливост, докази, верзии или пренасочување. Ги зачувува корисните дијагностички полиња, притоа создавајќи стабилен приказ за споредба.
<?php
// src/DetectionSnapshot.php
declare(strict_types=1);
namespace App;
final readonly class DetectionSnapshot
{
public function __construct(
public array $detections,
public array $redirects
) {}
public static function fromApi(array $payload): self
{
$items = $payload['detections'] ?? null;
$redirects = $payload['redirects'] ?? [];
if (!is_array($items) || !is_array($redirects)) {
throw new ApiFailure(
'protocol',
'Response detections and redirects must be arrays.'
);
}
$mapped = [];
foreach ($items as $item) {
if (!is_array($item)) {
throw new ApiFailure('protocol', 'Each detection must be an object.');
}
$name = $item['name'] ?? null;
$confidence = $item['confidence'] ?? null;
$evidence = $item['evidence'] ?? [];
$versions = $item['versions'] ?? [];
if (!is_string($name) || trim($name) === '') {
throw new ApiFailure('protocol', 'Detection name is missing.');
}
if ($confidence !== null && !is_numeric($confidence)) {
throw new ApiFailure('protocol', 'Confidence must be numeric.');
}
if (!is_array($evidence) || !is_array($versions)) {
throw new ApiFailure('protocol', 'Evidence and versions must be arrays.');
}
$versions = array_values(array_filter(
$versions,
static fn ($value): bool => is_string($value) && $value !== ''
));
sort($versions, SORT_STRING);
$mapped[strtolower(trim($name))] = [
'name' => trim($name),
'confidence' => $confidence === null ? null : (float) $confidence,
'evidence' => $evidence,
'versions' => $versions,
];
}
ksort($mapped, SORT_STRING);
return new self($mapped, array_values($redirects));
}
public function comparisonView(): array
{
return [
'technologies' => array_map(
static fn (array $item): array => [
'name' => $item['name'],
'versions' => $item['versions'],
],
$this->detections
),
'redirects' => $this->redirects,
];
}
public function toArray(): array
{
return [
'detections' => $this->detections,
'redirects' => $this->redirects,
'comparison' => $this->comparisonView(),
];
}
}
Одржувајте го овој мапер синхронизиран со шемата на одговорот прикажана во официјалната документација. Ако услугата се промени некомпатибилно, неуспехот на протоколот е побезбеден отколку тивко да се сними празен стек и да се пријави дека секоја технологија исчезнала.
Споредете снимки и испратете известувања
Командата ја валидира конфигурацијата, ги серијализира истовремените извршувања со заклучување на датотека, запишува состојба преку атомско преименување и евидентира оперативни настани преку syslog. Таа печати само промени што бараат дејство и неуспеси.
<?php
// bin/check-stack.php
declare(strict_types=1);
use App\CurlTransport;
use App\DetectionSnapshot;
use App\TechnologyDetectorClient;
require dirname(__DIR__) . '/vendor/autoload.php';
openlog('stack-monitor', LOG_PID, LOG_USER);
$token = getenv('WEBSITE_TECH_TOKEN') ?: '';
$stateDir = getenv('TECH_STATE_DIR') ?: '';
$urls = array_values(array_filter(array_map(
'trim',
explode(',', getenv('MONITORED_URLS') ?: '')
)));
if ($token === '' || $stateDir === '' || $urls === []) {
fwrite(STDOUT, "Stack monitor configuration is incomplete.\n");
exit(2);
}
if (!is_dir($stateDir) || !is_writable($stateDir)) {
fwrite(STDOUT, "State directory is missing or not writable.\n");
exit(2);
}
$client = new TechnologyDetectorClient(
new CurlTransport(),
$token,
static fn (int $microseconds) => usleep($microseconds)
);
$exit = 0;
foreach ($urls as $url) {
$id = hash('sha256', $url);
$stateFile = $stateDir . '/' . $id . '.json';
$lock = fopen($stateDir . '/' . $id . '.lock', 'c');
if ($lock === false || !flock($lock, LOCK_EX | LOCK_NB)) {
syslog(LOG_WARNING, "Skipped concurrent check url_hash={$id}");
continue;
}
try {
$snapshot = DetectionSnapshot::fromApi($client->detect($url));
$current = $snapshot->toArray();
$previous = null;
if (is_file($stateFile)) {
$previous = json_decode(
(string) file_get_contents($stateFile),
true,
512,
JSON_THROW_ON_ERROR
);
}
$temporary = $stateFile . '.' . getmypid() . '.tmp';
file_put_contents(
$temporary,
json_encode($current, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR),
LOCK_EX
);
rename($temporary, $stateFile);
if ($previous === null) {
syslog(LOG_INFO, "Baseline created url_hash={$id}");
} elseif (($previous['comparison'] ?? null) !== $current['comparison']) {
echo "Public technology stack changed: {$url}\n";
echo json_encode([
'before' => $previous['comparison'] ?? null,
'after' => $current['comparison'],
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), "\n";
$exit = max($exit, 10);
} else {
syslog(LOG_INFO, "No stack change url_hash={$id}");
}
} catch (Throwable $error) {
syslog(LOG_ERR, "Check failed url_hash={$id} type=" . $error::class);
echo "Technology check failed for {$url}: {$error->getMessage()}\n";
$exit = 20;
} finally {
flock($lock, LOCK_UN);
fclose($lock);
}
}
exit($exit);
Командата ја ажурира снимката по успешна споредба, така што една промена создава едно известување наместо повторливи предупредувања. Неуспешното барање никогаш не ја препишува последната доверлива состојба.
Тестирајте без повикување на живата услуга
Детерминистички лажен транспорт ја проверува најважната патека на неуспех: одговор ограничен по стапка повторно се обидува, додека токенот и телото на барањето сè уште се обработуваат од вистинскиот код на клиентот.
<?php
// tests/TechnologyDetectorClientTest.php
declare(strict_types=1);
use App\HttpResponse;
use App\TechnologyDetectorClient;
use App\Transport;
use PHPUnit\Framework\TestCase;
final class TechnologyDetectorClientTest extends TestCase
{
public function testRetriesRateLimitAndReturnsDecodedPayload(): void
{
$transport = new class implements Transport {
public int $calls = 0;
public function post(
string $url,
array $headers,
string $body,
int $connectTimeout,
int $responseTimeout
): HttpResponse {
$this->calls++;
if ($this->calls === 1) {
return new HttpResponse(429, '{}', ['retry-after' => '1']);
}
self::assertSame(['url' => 'https://client.example'], json_decode($body, true));
self::assertContains('Authorization: Bearer test-token', $headers);
self::assertSame(3, $connectTimeout);
self::assertSame(15, $responseTimeout);
return new HttpResponse(200, json_encode([
'detections' => [[
'name' => 'Example CMS',
'confidence' => 95,
'evidence' => ['header'],
'versions' => ['8.3'],
]],
'redirects' => [],
], JSON_THROW_ON_ERROR));
}
};
$delays = [];
$client = new TechnologyDetectorClient(
$transport,
'test-token',
static function (int $delay) use (&$delays): void {
$delays[] = $delay;
}
);
$result = $client->detect('https://client.example');
self::assertSame(2, $transport->calls);
self::assertSame([1_000_000], $delays);
self::assertSame('Example CMS', $result['detections'][0]['name']);
}
}
Извршете composer test. Додајте придружни тестови за непосреден неуспех 401, исцрпени повторни обиди за 503, невалиден JSON, неправилно форматирани откривања, почетната состојба при првото извршување и променета верзија. Овие случаи го штитат однесувањето што најверојатно ќе создаде пропуштени или погрешни известувања.
Распоредете безбедно и закажете го мониторот
Создадете го директориумот за состојба надвор од веб-коренот и ограничете го на сметката на оперативниот систем што ја извршува задачата. Не евидентирајте го токенот, заглавијата за авторизација, целосните API одговори или телата на одговорите од неуспеси. Командата евидентира URL-хеш во syslog бидејќи дури и јавниот hostname на клиентот може да биде чувствителна оперативна информација.
Запис во cron може да вчита датотека со променливи на околината читлива само за root и да испрати излез од командата по е-пошта:
[email protected]
17 * * * * set -a; . /etc/client-stack-monitor.env; set +a; cd /opt/client-stack-monitor && /usr/bin/php bin/check-stack.php
Изберете фреквенција што одговара на квотата на активираниот план. Излезниот код 0 значи успех или создавање почетна состојба, 10 значи откриена промена, а 20 значи дека барем една проверка не успеала. Е-поштата од cron го обезбедува известувањето, додека тие излезни кодови исто така чисто се интегрираат со закажувач или систем за мониторинг.
Чести продукциски неуспеси
- 401 или 403: проверете го внесениот токен и дали бил повторно генериран. Клиентот намерно не се обидува повторно при неуспеси на автентикација.
- 400 или 422: проверете ја конфигурираната URL-адреса и договорот за барањето. Неуспесите на валидацијата не се привремени.
- 429: намалете ја фреквенцијата на закажување или прегледајте го активниот план. Повторните обиди се ограничени и почитуваат краток нумерички
Retry-After. - Повторливи 5xx или истекувања: задржете ја последната почетна состојба, известете за неуспехот и дозволете следното закажано извршување повторно да се обиде.
- Неочекуван неуспех на протоколот: споредете ја официјалната документација за одговорот со
DetectionSnapshot::fromApi(); никогаш не ја ослабувајте валидацијата само за да ја потиснете грешката. - Нема е-пошта од cron: потврдете дека хостот има работен транспорт за пошта или насочете ги ненултите излези и стандардниот излез преку механизмот за мониторинг што веќе го користи серверот.
Контролна листа за конечна проверка
- Планот за услугата е активен, а токенот со опсег на услугата се внесува од околината.
- Минималното POST барање успева кон точната крајна точка за откривање.
composer testпоминува без мрежен пристап.- Првото извршување на командата создава состојба, но не испраќа известување за промена.
- Контролирана измена на снимката создава излезен код
10и видливи податоци пред и по промената. - Невалиден токен создава еден неуспех на автентикација без повторни обиди или губење на состојба.
- Директориумот за состојба и датотеката со променливи на околината се читливи само за сметката за распоредување.
- Syslog прима настани за успех и неуспех со URL-хеш, без акредитиви.
- Фреквенцијата на закажувачот останува во границите на избраниот план.
Доверливиот монитор за стек не се состои толку во испраќање чести барања, колку во зачувување доверлива состојба. Со валидирање на надворешните податоци, разликување трајни неуспеси од привремени, потиснување на шумот од докази и одбивање да се препише добра почетна состојба по грешка, оваа мала PHP услуга претвора промена на јавна веб-страница во корисен инженерски сигнал. Следниот пат кога клиент тивко ќе замени рамка, CMS или патека за пренасочување, откритието ќе пристигне како известување што бара дејство, наместо како непријатно изненадување.