Ticket Tracking¶
League Container usage¶
<?php
use League\Container\Container;
use Subscribed\IncidentMonitor\Container\IncidentMonitorServiceProvider;
use Subscribed\IncidentMonitor\Connector\Asana\AsanaConnector;
use Subscribed\IncidentMonitor\Connector\Asana\AsanaClientInterface;
use Subscribed\IncidentMonitor\Connector\Asana\AsanaFieldResolver;
use Subscribed\IncidentMonitor\Connector\Asana\AsanaHttpClient;
use Subscribed\IncidentMonitor\Connector\Asana\FixedFingerprintFieldGidProvider;
use Subscribed\IncidentMonitor\IncidentMonitor;
use Subscribed\IncidentMonitor\Value\TicketRequest;
// Create the application container and register the core service bindings.
$container = new Container();
$container->addServiceProvider(new IncidentMonitorServiceProvider());
// Resolve the Asana custom field used to store the fingerprint.
// This lets the connector look up an existing open ticket before creating a new one.
$fieldResolver = new AsanaFieldResolver(
$container->get(\Psr\Http\Client\ClientInterface::class),
$container->get(\Psr\Http\Message\RequestFactoryInterface::class),
$container->get(\Psr\Http\Message\StreamFactoryInterface::class),
'asana-personal-access-token',
$container->get(\Psr\SimpleCache\CacheInterface::class)
);
$fingerprintFieldGid = $fieldResolver->getFieldGidForProject('asana-project-id', 'fingerprint');
$fieldProvider = new FixedFingerprintFieldGidProvider($fingerprintFieldGid);
// Bind the concrete Asana API adapter used by the connector.
$container->add(AsanaClientInterface::class, function () use ($container, $fieldProvider) {
return new AsanaHttpClient(
$container->get(\Psr\Http\Client\ClientInterface::class),
$container->get(\Psr\Http\Message\RequestFactoryInterface::class),
$container->get(\Psr\Http\Message\StreamFactoryInterface::class),
'asana-personal-access-token',
$fieldProvider,
$container->get(\Psr\SimpleCache\CacheInterface::class)
);
});
// Register the connector so IncidentMonitor can resolve it by name.
$registry = $container->get(\Subscribed\IncidentMonitor\Connector\ConnectorRegistry::class);
$registry->register(new AsanaConnector($container->get(AsanaClientInterface::class)));
// Resolve the tracker service itself from the container.
$tracker = $container->get(IncidentMonitor::class);
// Build the ticket request. The fingerprint is what makes de-duplication stable.
$request = new TicketRequest(
'asana-project-id',
'Checkout failing for EU customers',
'Spike in 5xx in /checkout. Error code: PAY-42.',
'asana-user-id',
['incident', 'checkout'],
new DateTimeImmutable('+1 day'),
[
'service' => 'payments',
'environment' => 'production',
'fingerprint' => 'payments/not-captured/12345',
]
);
// Create a new ticket or return an existing matching one.
$result = $tracker->createOrGet('asana', $request);
De-duplication strategy¶
Default strategy uses a normalized fingerprint, preferring metadata['fingerprint'] when present. If not provided, it falls back to:
The fingerprint is used in two layers:
- Local fast cache via PSR-16
- Connector-level lookup for an existing open ticket
Recommended fingerprint shape:
If you want to enforce explicit fingerprints, bind:
Asana connector¶
AsanaConnector expects an adapter implementing AsanaClientInterface.
Minimum adapter responsibilities:
findOpenTaskByFingerprint(projectId, fingerprint)createTask(request, fingerprint)
Notes:
TicketRequest::tags()is treated as Asana tag GIDs byAsanaHttpClient- Open/closed is based on
completed=false - Fingerprint lookup uses
custom_fields.{gid}.value
Bedrock / shared container bootstrap¶
<?php
use Subscribed\IncidentMonitor\Container\IncidentMonitorBootstrap;
use Subscribed\IncidentMonitor\Facade\IncidentMonitorFacade;
// Register the entire package into the shared container.
IncidentMonitorBootstrap::register($container, [
// Optional: personal access token for the bundled Asana connector.
'asana_token' => 'asana-personal-access-token',
// Map your logical project keys to external Asana project IDs.
'project_map' => [
'customer-service' => 'asana-project-id-cs',
'warehouse' => 'asana-project-id-warehouse',
'development' => 'asana-project-id-dev',
],
// Used when createDefault() is called or when health checks omit project_key.
'default_project_key' => 'development',
// Cache window for local de-duplication results.
'dedupe_ttl_seconds' => 86400,
]);
// Expose the container to the static facade API.
IncidentMonitorFacade::setContainer($container);
This bootstrap:
- Adds the service provider
- Registers the Asana connector when
asana_tokenis configured - Sets the strict fingerprint strategy by default
Concurrency lock¶
The library uses a short-lived per-fingerprint lock to prevent duplicate creates under concurrent load.
Defaults:
lock_ttl_seconds:30lock_wait_ms:1500lock_wait_interval_ms:200
If no PSR-16 cache is bound, the bootstrap uses:
- WordPress transients when available
- File cache in
sys_get_temp_dir() . '/incident-monitor-cache'otherwise
Override with:
Guzzle 7 wiring¶
<?php
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Psr7\HttpFactory;
use Subscribed\IncidentMonitor\Http\IncidentMonitorHttpClientInterface;
// Bind a dedicated HTTP client for IncidentMonitor so it stays isolated from other integrations.
$container->addShared(IncidentMonitorHttpClientInterface::class, function () {
return new GuzzleClient();
});
// Bind the PSR-17 factories used by the Asana HTTP client.
$container->addShared(\Psr\Http\Message\RequestFactoryInterface::class, function () {
return new HttpFactory();
});
$container->addShared(\Psr\Http\Message\StreamFactoryInterface::class, function () {
return new HttpFactory();
});
Facade usage¶
<?php
use Subscribed\IncidentMonitor\Facade\IncidentMonitorFacade;
// Resolve the external project through the configured project map and create the ticket.
IncidentMonitorFacade::createFor(
'asana',
'customer-service',
'Uncaptured payments detected',
'Payment 12345 was authorized but not captured.',
'asana-user-id',
['incident', 'payments'],
null,
['fingerprint' => 'payments/not-captured/12345']
);
Safe usage:
<?php
// tryCreate() returns null if the facade has not been bootstrapped yet.
$result = IncidentMonitorFacade::tryCreateFor(
'asana',
'customer-service',
'Uncaptured payments detected',
'Payment 12345 was authorized but not captured.',
null,
[],
null,
['fingerprint' => 'payments/not-captured/12345']
);
Default project usage:
<?php
// createDefault() uses the configured default_project_key from bootstrap config.
IncidentMonitorFacade::createDefaultFor(
'asana',
'Warehouse outage',
'No pick list generated for 15 minutes.',
null,
['incident', 'warehouse'],
null,
['fingerprint' => 'warehouse/pick-list/missing-15m']
);
WP-CLI¶
The package exposes incident commands through WP-CLI when the package and facade bootstrap are loaded:
wp subscribed incidents create "Daily import failed" "The ERP import failed for shop 42." \
--connector=asana \
--project-key=operations \
--tag=incident \
--tag=erp-import \
--fingerprint=erp-import/shop-42/daily-job
This can also be queued instead of executed inline:
wp subscribed incidents queue "Daily import failed" "The ERP import failed for shop 42." \
--connector=asana \
--project-key=operations \
--fingerprint=erp-import/shop-42/daily-job
Issue tracking setup with IncidentMonitorFacade¶
This is the smallest setup for packages that just want to emit incidents through the facade.
<?php
use League\Container\Container;
use Subscribed\IncidentMonitor\Container\IncidentMonitorBootstrap;
use Subscribed\IncidentMonitor\Facade\IncidentMonitorFacade;
$container = new Container();
// Register connector, cache, lock, and project resolution.
IncidentMonitorBootstrap::register($container, [
'asana_token' => getenv('ASANA_TOKEN'),
'project_map' => [
'development' => 'asana-project-id-dev',
'operations' => 'asana-project-id-ops',
],
'default_project_key' => 'development',
'dedupe_ttl_seconds' => 86400,
]);
// Make the facade available to the rest of the application.
IncidentMonitorFacade::setContainer($container);
// Later in your package code, emit an incident through the static facade.
IncidentMonitorFacade::createFor(
'asana',
'operations',
'Daily import failed',
'The ERP import failed for shop 42.',
null,
['incident', 'erp-import'],
null,
[
// Keep this stable for the same incident so duplicates collapse correctly.
'fingerprint' => 'erp-import/shop-42/daily-job',
'shop_id' => 42,
]
);