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
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,42 @@ $media = $client->validate()->media('https://yourbrand.com/launch.png');
$media->ok; // 200 even when a check fails; read $media->issues
```

## Analytics

```php
// How long a post keeps earning, from the repeated readings of each post
$decay = $client->analytics()->decay(days: 30);
echo $decay->halfLifeBucket; // e.g. "1h_3h"

// Whether posting more earned more
$cadence = $client->analytics()->frequency(days: 90);
echo $cadence->best?->label; // e.g. "3-5 a week"

// Every reading held for one post, with what moved between them
$timeline = $client->analytics()->timeline($post->id);

// Mirror the metrics into your own store, without refetching everything
$cursor = null;
do {
$page = $client->analytics()->changes(since: $cursor);
save($page->changes);
$cursor = $page->cursor?->format(DATE_ATOM);
} while ($page->hasMore && $cursor !== null);

// Refresh one post now instead of waiting for the next collection run
$client->analytics()->collectPost($post->id);

// Posts on the account that never went out through FoPost
foreach ($client->analytics()->nativePosts($accounts[0]->id)->items as $native) {
echo $native->permalink, ' ', $native->metrics->engagements, PHP_EOL;
}
```

A post is addressed by its FoPost id or by its permalink, so a post made by
hand on the network works the same way:

```php
$client->analytics()->timeline('https://x.com/acme/status/1');
## Activity

What happened in a workspace, newest first. Needs the `analytics` scope.
Expand Down
8 changes: 8 additions & 0 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Fopost\Sdk\Resource\AccountGroupsResource;
use Fopost\Sdk\Resource\AccountsResource;
use Fopost\Sdk\Resource\AdsResource;
use Fopost\Sdk\Resource\AnalyticsResource;
use Fopost\Sdk\Resource\AiResource;
use Fopost\Sdk\Resource\BroadcastsResource;
use Fopost\Sdk\Resource\ContactsResource;
Expand Down Expand Up @@ -56,6 +57,7 @@ final class Client
private readonly AdsResource $ads;
private readonly MediaResource $media;
private readonly ValidateResource $validate;
private readonly AnalyticsResource $analytics;
private readonly GoogleBusinessResource $googleBusiness;

public function __construct(
Expand Down Expand Up @@ -89,6 +91,7 @@ public function __construct(
$this->ads = new AdsResource($this->http);
$this->media = new MediaResource($this->http);
$this->validate = new ValidateResource($this->http);
$this->analytics = new AnalyticsResource($this->http);
$this->googleBusiness = new GoogleBusinessResource($this->http);
}

Expand Down Expand Up @@ -167,6 +170,11 @@ public function validate(): ValidateResource
return $this->validate;
}

public function analytics(): AnalyticsResource
{
return $this->analytics;
}

/** Manage a connected Google Business Profile location. */
public function googleBusiness(): GoogleBusinessResource
{
Expand Down
39 changes: 39 additions & 0 deletions src/Model/CollectPostDelivery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace Fopost\Sdk\Model;

use DateTimeImmutable;

/** What the on-demand refresh did for one delivery. */
final class CollectPostDelivery extends Model
{
private function __construct(
array $raw,
public readonly string $accountId,
public readonly string $platform,
public readonly string $externalPostId,
public readonly bool $collected,
public readonly ?DateTimeImmutable $fetchedAt,
/** Why the refresh did not happen. */
public readonly ?string $message,
) {
parent::__construct($raw);
}

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

return new self(
$data,
self::requiredStr($data, 'account_id'),
self::requiredStr($data, 'platform'),
self::requiredStr($data, 'external_post_id'),
self::bool($data, 'collected') ?? false,
self::date($data, 'fetched_at'),
self::str($data, 'message'),
);
}
}
29 changes: 29 additions & 0 deletions src/Model/CollectPostResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Fopost\Sdk\Model;

/** What POST /posts/{id}/analytics/collect returns. */
final class CollectPostResult extends Model
{
/** @param array<int, CollectPostDelivery> $deliveries */
private function __construct(
array $raw,
public readonly int $collected,
public readonly array $deliveries,
) {
parent::__construct($raw);
}

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

return new self(
$data,
self::int($data, 'collected') ?? 0,
CollectPostDelivery::listFrom(self::seq($data, 'deliveries')),
);
}
}
34 changes: 34 additions & 0 deletions src/Model/ContentDecayReport.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace Fopost\Sdk\Model;

/** How engagement accumulates as a post ages. */
final class ContentDecayReport extends Model
{
/** @param array<int, DecayBand> $bands */
private function __construct(
array $raw,
public readonly int $days,
public readonly int $postsMeasured,
/** First band where the average post had passed half its final engagement. */
public readonly ?string $halfLifeBucket,
public readonly array $bands,
) {
parent::__construct($raw);
}

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

return new self(
$data,
self::int($data, 'days') ?? 0,
self::int($data, 'posts_measured') ?? 0,
self::str($data, 'half_life_bucket'),
DecayBand::listFrom(self::seq($data, 'bands')),
);
}
}
38 changes: 38 additions & 0 deletions src/Model/DecayBand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace Fopost\Sdk\Model;

/** One age band of GET /analytics/decay. */
final class DecayBand extends Model
{
private function __construct(
array $raw,
public readonly string $bucket,
public readonly string $label,
/** Posts with at least one reading in this band. */
public readonly int $posts,
public readonly float $avgEngagements,
public readonly float $avgImpressions,
/** Mean share of the post's final engagement reached by this age, 0-1. */
public readonly ?float $shareOfFinal,
) {
parent::__construct($raw);
}

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

return new self(
$data,
self::requiredStr($data, 'bucket'),
self::requiredStr($data, 'label'),
self::int($data, 'posts') ?? 0,
self::num($data, 'avg_engagements') ?? 0.0,
self::num($data, 'avg_impressions') ?? 0.0,
self::num($data, 'share_of_final'),
);
}
}
39 changes: 39 additions & 0 deletions src/Model/FrequencyBand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace Fopost\Sdk\Model;

/** One cadence band of GET /analytics/frequency. */
final class FrequencyBand extends Model
{
private function __construct(
array $raw,
public readonly string $band,
public readonly string $label,
public readonly int $weeks,
public readonly int $posts,
public readonly float $avgPostsPerWeek,
public readonly float $avgEngagementsPerPost,
/** Engagements over reach, impressions as the stand-in; null with neither. */
public readonly ?float $engagementRate,
) {
parent::__construct($raw);
}

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

return new self(
$data,
self::requiredStr($data, 'band'),
self::requiredStr($data, 'label'),
self::int($data, 'weeks') ?? 0,
self::int($data, 'posts') ?? 0,
self::num($data, 'avg_posts_per_week') ?? 0.0,
self::num($data, 'avg_engagements_per_post') ?? 0.0,
self::num($data, 'engagement_rate'),
);
}
}
33 changes: 33 additions & 0 deletions src/Model/FrequencyWeek.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Fopost\Sdk\Model;

/** One week of GET /analytics/frequency. */
final class FrequencyWeek extends Model
{
private function __construct(
array $raw,
/** Monday of the week, UTC, as YYYY-MM-DD. */
public readonly string $weekStart,
public readonly int $posts,
public readonly int $engagements,
public readonly float $avgEngagementsPerPost,
) {
parent::__construct($raw);
}

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

return new self(
$data,
self::requiredStr($data, 'week_start'),
self::int($data, 'posts') ?? 0,
self::int($data, 'engagements') ?? 0,
self::num($data, 'avg_engagements_per_post') ?? 0.0,
);
}
}
51 changes: 51 additions & 0 deletions src/Model/MetricChange.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace Fopost\Sdk\Model;

use DateTimeImmutable;

/** One metric reading, as the changes feed reports it. */
final class MetricChange extends Model
{
private function __construct(
array $raw,
public readonly string $accountId,
public readonly string $platform,
public readonly string $externalPostId,
/** Null for a post made natively on the network. */
public readonly ?string $postId,
public readonly ?DateTimeImmutable $postedAt,
public readonly ?DateTimeImmutable $fetchedAt,
public readonly ?int $impressions,
public readonly ?int $reach,
public readonly ?int $engagements,
public readonly ?int $likes,
public readonly ?int $comments,
public readonly ?int $shares,
) {
parent::__construct($raw);
}

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

return new self(
$data,
self::requiredStr($data, 'account_id'),
self::requiredStr($data, 'platform'),
self::requiredStr($data, 'external_post_id'),
self::str($data, 'post_id'),
self::date($data, 'posted_at'),
self::date($data, 'fetched_at'),
self::int($data, 'impressions'),
self::int($data, 'reach'),
self::int($data, 'engagements'),
self::int($data, 'likes'),
self::int($data, 'comments'),
self::int($data, 'shares'),
);
}
}
36 changes: 36 additions & 0 deletions src/Model/MetricChangePage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Fopost\Sdk\Model;

use DateTimeImmutable;

/** Readings since a cursor, with the cursor to pass next time. */
final class MetricChangePage extends Model
{
/** @param array<int, MetricChange> $changes */
private function __construct(
array $raw,
public readonly ?DateTimeImmutable $since,
/** Feed back as `since` to continue; null when nothing changed. */
public readonly ?DateTimeImmutable $cursor,
public readonly bool $hasMore,
public readonly array $changes,
) {
parent::__construct($raw);
}

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

return new self(
$data,
self::date($data, 'since'),
self::date($data, 'cursor'),
self::bool($data, 'has_more') ?? false,
MetricChange::listFrom(self::seq($data, 'changes')),
);
}
}
Loading
Loading