Skip to content

Commit 7f6ce15

Browse files
authored
Merge pull request #150 from andriichuk/feature-outcomes
Implement View Outcomes endpoint
2 parents 47b3b78 + 3b490ba commit 7f6ce15

File tree

7 files changed

+276
-12
lines changed

7 files changed

+276
-12
lines changed

README.md

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,24 @@ Delete Segments ([official documentation](https://documentation.onesignal.com/re
9696
$oneSignal->apps()->deleteSegment('application_id', 'segment_id');
9797
```
9898

99+
View the details of all the outcomes associated with your app ([official documentation](https://documentation.onesignal.com/reference/view-outcomes)):
100+
101+
```php
102+
use OneSignal\Apps;
103+
use OneSignal\Devices;
104+
105+
$outcomes = $oneSignal->apps()->outcomes('application_id', [
106+
'outcome_names' => [
107+
'os__session_duration.count',
108+
'os__click.count',
109+
'Sales, Purchase.sum',
110+
],
111+
'outcome_time_range' => Apps::OUTCOME_TIME_RANGE_MONTH,
112+
'outcome_platforms' => [Devices::IOS, Devices::ANDROID],
113+
'outcome_attribution' => Apps::OUTCOME_ATTRIBUTION_DIRECT,
114+
]);
115+
```
116+
99117
### Devices API
100118

101119
View the details of multiple devices in one of your OneSignal apps ([official documentation](https://documentation.onesignal.com/reference#view-devices)):
@@ -113,36 +131,34 @@ $device = $oneSignal->devices()->getOne('device_id');
113131
Register a new device to your configured OneSignal application ([official documentation](https://documentation.onesignal.com/reference#add-a-device)):
114132

115133
```php
116-
use OneSignal\Api\Devices;
134+
use OneSignal\Devices;
117135

118136
$newDevice = $oneSignal->devices()->add([
119137
'device_type' => Devices::ANDROID,
120138
'identifier' => 'abcdefghijklmn',
121139
]);
122140
```
123141

124-
Update an existing device's tags in one of your OneSignal apps using the External User ID ([official documentation](https://documentation.onesignal.com/reference/edit-tags-with-external-user-id)):
142+
Update an existing device in your configured OneSignal application ([official documentation](https://documentation.onesignal.com/reference#edit-device)):
125143

126144
```php
127-
use OneSignal\Api\Devices;
145+
$oneSignal->devices()->update('device_id', [
146+
'session_count' => 2,
147+
]);
148+
```
149+
150+
Update an existing device's tags in one of your OneSignal apps using the External User ID ([official documentation](https://documentation.onesignal.com/reference/edit-tags-with-external-user-id)):
128151

152+
```php
129153
$externalUserId = '12345';
130-
$newDevice = $oneSignal->devices()->editTags($externalUserId, [
154+
$response = $oneSignal->devices()->editTags($externalUserId, [
131155
'tags' => [
132156
'a' => '1',
133157
'foo' => '',
134158
],
135159
]);
136160
```
137161

138-
Update an existing device in your configured OneSignal application ([official documentation](https://documentation.onesignal.com/reference#edit-device)):
139-
140-
```php
141-
$oneSignal->devices()->update('device_id', [
142-
'session_count' => 2,
143-
]);
144-
```
145-
146162
### Notifications API
147163

148164
View the details of multiple notifications ([official documentation](https://documentation.onesignal.com/reference#view-notifications)):

src/Apps.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@
88

99
class Apps extends AbstractApi
1010
{
11+
public const OUTCOME_ATTRIBUTION_TOTAL = 'total';
12+
public const OUTCOME_ATTRIBUTION_UNATTRIBUTED = 'unattributed';
13+
public const OUTCOME_ATTRIBUTION_INFLUENCED = 'influenced';
14+
public const OUTCOME_ATTRIBUTION_DIRECT = 'direct';
15+
16+
public const OUTCOME_TIME_RANGE_MONTH = '1mo';
17+
public const OUTCOME_TIME_RANGE_HOUR = '1h';
18+
public const OUTCOME_TIME_RANGE_DAY = '1d';
19+
1120
private $resolverFactory;
1221

1322
public function __construct(OneSignal $client, ResolverFactory $resolverFactory)
@@ -117,4 +126,22 @@ public function deleteSegment(string $appId, string $segmentId): array
117126

118127
return $this->client->sendRequest($request);
119128
}
129+
130+
/**
131+
* View the details of all the outcomes associated with your app.
132+
*
133+
* @param string $appId Application ID
134+
* @param array $data Outcome data filters
135+
*/
136+
public function outcomes(string $appId, array $data): array
137+
{
138+
$resolvedData = $this->resolverFactory->createOutcomesResolver()->resolve($data);
139+
140+
$queryString = preg_replace('/%5B\d+%5D/', '%5B%5D', http_build_query($resolvedData));
141+
142+
$request = $this->createRequest('GET', "/apps/$appId/outcomes?$queryString");
143+
$request = $request->withHeader('Authorization', "Basic {$this->client->getConfig()->getApplicationAuthKey()}");
144+
145+
return $this->client->sendRequest($request);
146+
}
120147
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OneSignal\Resolver;
6+
7+
use OneSignal\Apps;
8+
use OneSignal\Devices;
9+
use Symfony\Component\OptionsResolver\Options;
10+
use Symfony\Component\OptionsResolver\OptionsResolver;
11+
12+
class AppOutcomesResolver implements ResolverInterface
13+
{
14+
/**
15+
* {@inheritdoc}
16+
*/
17+
public function resolve(array $data): array
18+
{
19+
return (new OptionsResolver())
20+
->setDefined('outcome_names')
21+
->setAllowedTypes('outcome_names', 'string[]')
22+
->setDefined('outcome_time_range')
23+
->setAllowedTypes('outcome_time_range', 'string')
24+
->setAllowedValues('outcome_time_range', [Apps::OUTCOME_TIME_RANGE_HOUR, Apps::OUTCOME_TIME_RANGE_DAY, Apps::OUTCOME_TIME_RANGE_MONTH])
25+
->setDefault('outcome_time_range', Apps::OUTCOME_TIME_RANGE_HOUR)
26+
->setDefined('outcome_platforms')
27+
->setAllowedTypes('outcome_platforms', 'int[]')
28+
->setAllowedValues('outcome_platforms', static function (array $platforms): bool {
29+
$intersect = array_intersect($platforms, [
30+
Devices::IOS,
31+
Devices::ANDROID,
32+
Devices::AMAZON,
33+
Devices::WINDOWS_PHONE,
34+
Devices::WINDOWS_PHONE_MPNS,
35+
Devices::CHROME_APP,
36+
Devices::CHROME_WEB,
37+
Devices::WINDOWS_PHONE_WNS,
38+
Devices::SAFARI,
39+
Devices::FIREFOX,
40+
Devices::MACOS,
41+
Devices::ALEXA,
42+
Devices::EMAIL,
43+
Devices::HUAWEI,
44+
Devices::SMS,
45+
]);
46+
47+
return count($intersect) === count($platforms);
48+
})
49+
->setNormalizer('outcome_platforms', static function (Options $options, array $value): string {
50+
return implode(',', $value);
51+
})
52+
->setDefined('outcome_attribution')
53+
->setAllowedTypes('outcome_attribution', 'string')
54+
->setAllowedValues('outcome_attribution', [
55+
Apps::OUTCOME_ATTRIBUTION_TOTAL,
56+
Apps::OUTCOME_ATTRIBUTION_DIRECT,
57+
Apps::OUTCOME_ATTRIBUTION_INFLUENCED,
58+
Apps::OUTCOME_ATTRIBUTION_UNATTRIBUTED,
59+
])
60+
->setDefault('outcome_attribution', Apps::OUTCOME_ATTRIBUTION_TOTAL)
61+
->setRequired(['outcome_names'])
62+
->resolve($data);
63+
}
64+
}

src/Resolver/ResolverFactory.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ public function createSegmentResolver(): SegmentResolver
2525
return new SegmentResolver();
2626
}
2727

28+
public function createOutcomesResolver(): AppOutcomesResolver
29+
{
30+
return new AppOutcomesResolver();
31+
}
32+
2833
public function createDeviceSessionResolver(): DeviceSessionResolver
2934
{
3035
return new DeviceSessionResolver();

tests/AppsTest.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace OneSignal\Tests;
66

77
use OneSignal\Apps;
8+
use OneSignal\Devices;
89
use OneSignal\OneSignal;
910
use OneSignal\Resolver\ResolverFactory;
1011
use Symfony\Component\HttpClient\Response\MockResponse;
@@ -437,4 +438,51 @@ public function testCreateSegmentWithEmptyName(): void
437438
'errors' => ['name is required'],
438439
], $responseData);
439440
}
441+
442+
public function testOutcomes(): void
443+
{
444+
$client = $this->createClientMock(function (string $method, string $url, array $options): ResponseInterface {
445+
$this->assertSame('GET', $method);
446+
$this->assertSame(OneSignal::API_URL.'/apps/fakeApplicationId/outcomes?outcome_time_range=1h&outcome_attribution=direct&outcome_names%5B%5D=os__session_duration.count&outcome_names%5B%5D=os__click.count&outcome_names%5B%5D=Sales%2C+Purchase.sum&outcome_platforms=0%2C1', $url);
447+
$this->assertArrayHasKey('accept', $options['normalized_headers']);
448+
$this->assertArrayHasKey('authorization', $options['normalized_headers']);
449+
$this->assertSame('Accept: application/json', $options['normalized_headers']['accept'][0]);
450+
$this->assertSame('Authorization: Basic fakeApplicationAuthKey', $options['normalized_headers']['authorization'][0]);
451+
452+
return new MockResponse($this->loadFixture('apps_outcomes.json'), ['http_code' => 200]);
453+
});
454+
455+
$apps = new Apps($client, new ResolverFactory($client->getConfig()));
456+
457+
$responseData = $apps->outcomes('fakeApplicationId', [
458+
'outcome_names' => [
459+
'os__session_duration.count',
460+
'os__click.count',
461+
'Sales, Purchase.sum',
462+
],
463+
'outcome_time_range' => '1h',
464+
'outcome_platforms' => [Devices::IOS, Devices::ANDROID],
465+
'outcome_attribution' => Apps::OUTCOME_ATTRIBUTION_DIRECT,
466+
]);
467+
468+
self::assertSame([
469+
'outcomes' => [
470+
[
471+
'id' => 'os__session_duration',
472+
'value' => 100,
473+
'aggregation' => 'sum',
474+
],
475+
[
476+
'id' => 'os__click',
477+
'value' => 4,
478+
'aggregation' => 'count',
479+
],
480+
[
481+
'id' => 'Sales, Purchase.count',
482+
'value' => 348,
483+
'aggregation' => 'sum',
484+
],
485+
],
486+
], $responseData);
487+
}
440488
}

tests/Fixtures/apps_outcomes.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"outcomes": [
3+
{
4+
"id": "os__session_duration",
5+
"value": 100,
6+
"aggregation": "sum"
7+
},
8+
{
9+
"id": "os__click",
10+
"value": 4,
11+
"aggregation": "count"
12+
},
13+
{
14+
"id": "Sales, Purchase.count",
15+
"value": 348,
16+
"aggregation": "sum"
17+
}
18+
]
19+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OneSignal\Tests\Resolver;
6+
7+
use OneSignal\Resolver\AppOutcomesResolver;
8+
use PHPUnit\Framework\TestCase;
9+
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
10+
use Symfony\Component\OptionsResolver\Exception\MissingOptionsException;
11+
use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException;
12+
13+
class AppOutcomeResolverTest extends TestCase
14+
{
15+
/**
16+
* @var AppOutcomesResolver
17+
*/
18+
private $appResolver;
19+
20+
protected function setUp(): void
21+
{
22+
$this->appResolver = new AppOutcomesResolver();
23+
}
24+
25+
public function testResolveWithValidValues(): void
26+
{
27+
$expectedData = [
28+
'outcome_names' => [
29+
'os__session_duration.count',
30+
'os__click.count',
31+
],
32+
'outcome_time_range' => '1mo',
33+
'outcome_platforms' => [0, 1, 2],
34+
'outcome_attribution' => 'direct',
35+
];
36+
37+
self::assertEquals(
38+
array_merge($expectedData, [
39+
'outcome_platforms' => '0,1,2',
40+
]),
41+
$this->appResolver->resolve($expectedData)
42+
);
43+
}
44+
45+
public function testResolveWithMissingRequiredValue(): void
46+
{
47+
$this->expectException(MissingOptionsException::class);
48+
49+
$this->appResolver->resolve([]);
50+
}
51+
52+
public function wrongValueTypesProvider(): iterable
53+
{
54+
yield [['outcome_names' => 100]];
55+
yield [['outcome_names' => [1, 2]]];
56+
yield [['outcome_time_range' => 1]];
57+
yield [['outcome_time_range' => '2d']];
58+
yield [['outcome_platforms' => 0]];
59+
yield [['outcome_platforms' => ['0']]];
60+
yield [['outcome_platforms' => [100]]];
61+
yield [['outcome_attribution' => []]];
62+
yield [['outcome_attribution' => 'indirect']];
63+
}
64+
65+
/**
66+
* @dataProvider wrongValueTypesProvider
67+
*/
68+
public function testResolveWithWrongValueTypes(array $wrongOption): void
69+
{
70+
$this->expectException(InvalidOptionsException::class);
71+
72+
$requiredOptions = [
73+
'outcome_names' => ['os__click.count'],
74+
];
75+
76+
$this->appResolver->resolve(array_merge($requiredOptions, $wrongOption));
77+
}
78+
79+
public function testResolveWithWrongOption(): void
80+
{
81+
$this->expectException(UndefinedOptionsException::class);
82+
83+
$this->appResolver->resolve(['wrongOption' => 'wrongValue']);
84+
}
85+
}

0 commit comments

Comments
 (0)