Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ $account = $client->accounts()->get('acc_1');
$health = $client->accounts()->health('acc_1');
$client->accounts()->disconnect('acc_1');

// The numbers only this account's network reports, in its own vocabulary.
$metrics = $client->accounts()->platformMetrics('acc_1');
foreach ($metrics->account->metrics as $row) {
echo "{$row->label}: {$row->value}\n";
}

// Rename; null restores the platform name.
$client->accounts()->update('acc_1', 'Brand HQ');
$client->accounts()->move('acc_1', $otherWorkspaceId);
Expand Down
30 changes: 30 additions & 0 deletions src/Model/AccountPlatformMetrics.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Fopost\Sdk\Model;

/** What only this network reports, in its own vocabulary. */
final class AccountPlatformMetrics extends Model
{
private function __construct(
array $raw,
public readonly string $platform,
public readonly PlatformMetricsBlock $account,
public readonly PlatformMetricsBlock $post,
) {
parent::__construct($raw);
}

public static function fromArray(mixed $data): static
{
$data = is_array($data) ? $data : [];

return new self(
$data,
self::str($data, 'platform') ?? '',
PlatformMetricsBlock::fromArray(self::nested($data, 'account') ?? []),
PlatformMetricsBlock::fromArray(self::nested($data, 'post') ?? []),
);
}
}
36 changes: 36 additions & 0 deletions src/Model/PlatformMetricRow.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Fopost\Sdk\Model;

/** One metric a network reports under its own name. */
final class PlatformMetricRow extends Model
{
private function __construct(
array $raw,
/** The platform's own metric name. Stable — read this, not $label. */
public readonly string $key,
/** Ours, and subject to rewording. */
public readonly string $label,
/** count | duration_ms | currency_usd | ratio | series */
public readonly string $kind,
/** A number for every kind but `series`, which is a list of points. */
public readonly mixed $value,
) {
parent::__construct($raw);
}

public static function fromArray(mixed $data): static
{
$data = is_array($data) ? $data : [];

return new self(
$data,
self::requiredStr($data, 'key'),
self::str($data, 'label') ?? '',
self::str($data, 'kind') ?? 'count',
self::field($data, 'value'),
);
}
}
32 changes: 32 additions & 0 deletions src/Model/PlatformMetricsBlock.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace Fopost\Sdk\Model;

/** One side of a per-network metric set: the account itself, or its newest measured post. */
final class PlatformMetricsBlock extends Model
{
private function __construct(
array $raw,
public readonly ?string $fetchedAt,
/** Null on the account block, and on a network that reports nothing per post. */
public readonly ?string $externalPostId,
/** @var array<int, PlatformMetricRow> */
public readonly array $metrics,
) {
parent::__construct($raw);
}

public static function fromArray(mixed $data): static
{
$data = is_array($data) ? $data : [];

return new self(
$data,
self::str($data, 'fetched_at'),
self::str($data, 'external_post_id'),
PlatformMetricRow::listFrom(self::seq($data, 'metrics')),
);
}
}
18 changes: 18 additions & 0 deletions src/Resource/AccountsResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Fopost\Sdk\Resource;

use Fopost\Sdk\Model\AccountMove;
use Fopost\Sdk\Model\AccountPlatformMetrics;
use Fopost\Sdk\Model\AccountRename;
use Fopost\Sdk\Model\DiscordChannel;
use Fopost\Sdk\Model\DiscordIdentity;
Expand Down Expand Up @@ -85,6 +86,23 @@ public function health(string $accountId): array
return self::asArray(self::unwrap($this->http->get("/accounts/{$accountId}/health")));
}

/**
* The numbers only this account's network reports, in its own vocabulary.
*
* Ad-break earnings, story taps, a retention curve, the search terms behind a
* listing — keyed by the platform's own metric names, read from the newest
* collected snapshot rather than fetched live. Needs the `analytics` scope.
*
* A network whose metric access has not been granted yet answers 503
* (`platform_metrics_unavailable`) rather than an empty set.
*/
public function platformMetrics(string $accountId): AccountPlatformMetrics
{
return AccountPlatformMetrics::fromArray(
self::unwrap($this->http->get("/accounts/{$accountId}/insights", ['raw' => 'true'])),
);
}

/** Rename the account; null or an empty string restores the platform name. */
public function update(string $accountId, ?string $displayName): AccountRename
{
Expand Down
95 changes: 95 additions & 0 deletions tests/PlatformMetricsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

declare(strict_types=1);

namespace Fopost\Sdk\Tests;

use Fopost\Sdk\Exception\ApiException;

final class PlatformMetricsTest extends TestCase
{
public function testPlatformMetricsAsksForRawAndParsesTheSet(): void
{
$this->transport->push(200, ['data' => [
'platform' => 'facebook',
'account' => [
'fetched_at' => '2026-09-20T02:00:00.000Z',
'metrics' => [
[
'key' => 'page_daily_video_ad_break_earnings',
'label' => 'Ad Break Earnings',
'kind' => 'currency_usd',
'value' => 42.15,
],
[
'key' => 'page_impressions_paid',
'label' => 'Paid Impressions',
'kind' => 'count',
'value' => 1500,
],
],
],
'post' => [
'external_post_id' => '123_456',
'fetched_at' => '2026-09-20T02:00:00.000Z',
'metrics' => [],
],
]]);

$metrics = $this->client()->accounts()->platformMetrics('a_1');

$this->assertSame(
'https://api.fopost.com/v1/accounts/a_1/insights?raw=true',
$this->transport->last()['url'],
);
$this->assertSame('facebook', $metrics->platform);
$this->assertSame('2026-09-20T02:00:00.000Z', $metrics->account->fetchedAt);
$this->assertCount(2, $metrics->account->metrics);
$this->assertSame('page_daily_video_ad_break_earnings', $metrics->account->metrics[0]->key);
$this->assertSame('currency_usd', $metrics->account->metrics[0]->kind);
$this->assertSame(42.15, $metrics->account->metrics[0]->value);
$this->assertSame('123_456', $metrics->post->externalPostId);
$this->assertSame([], $metrics->post->metrics);
}

public function testASeriesValueSurvivesAsAList(): void
{
$this->transport->push(200, ['data' => [
'platform' => 'youtube',
'account' => [
'fetched_at' => null,
'metrics' => [[
'key' => 'daily_views',
'label' => 'Views by Day',
'kind' => 'series',
'value' => [['day' => '2026-09-19', 'views' => 600]],
]],
],
'post' => ['external_post_id' => null, 'fetched_at' => null, 'metrics' => []],
]]);

$metrics = $this->client()->accounts()->platformMetrics('a_1');

$this->assertSame(
[['day' => '2026-09-19', 'views' => 600]],
$metrics->account->metrics[0]->value,
);
$this->assertNull($metrics->account->fetchedAt);
}

public function testAPendingMetricGrantThrows(): void
{
$this->transport->push(503, [
'error' => 'platform_metrics_unavailable',
'message' => 'google-business metrics are not available on this deployment yet.',
]);

try {
$this->client(1)->accounts()->platformMetrics('a_1');
$this->fail('Expected an ApiException');
} catch (ApiException $e) {
$this->assertSame(503, $e->status);
$this->assertSame('platform_metrics_unavailable', $e->errorCode);
}
}
}
Loading