File Coverage

blib/lib/WebService/Async/CustomerIO.pm
Criterion Covered Total %
statement 96 106 90.5
branch 27 38 71.0
condition 4 10 40.0
subroutine 32 35 91.4
pod 11 13 84.6
total 170 202 84.1


line stmt bran cond sub pod time code
1             package WebService::Async::CustomerIO;
2              
3 3     3   327438 use strict;
  3         22  
  3         91  
4 3     3   18 use warnings;
  3         7  
  3         142  
5              
6             our $VERSION = '0.001';
7              
8             =head1 NAME
9              
10             WebService::Async::CustomerIO - unofficial support for the Customer.io service
11              
12             =head1 SYNOPSIS
13              
14             =head1 DESCRIPTION
15              
16             =cut
17              
18 3     3   1315 use parent qw(IO::Async::Notifier);
  3         960  
  3         18  
19              
20 3     3   12718 use mro;
  3         7  
  3         29  
21 3     3   1606 use Syntax::Keyword::Try;
  3         6673  
  3         16  
22 3     3   248 use Future;
  3         7  
  3         63  
23 3     3   1713 use Net::Async::HTTP;
  3         354656  
  3         123  
24 3     3   29 use Carp qw();
  3         7  
  3         65  
25 3     3   1349 use JSON::MaybeUTF8 qw(:v1);
  3         23774  
  3         408  
26 3     3   32 use URI::Escape;
  3         7  
  3         169  
27              
28 3     3   1648 use WebService::Async::CustomerIO::Customer;
  3         9  
  3         105  
29 3     3   1269 use WebService::Async::CustomerIO::RateLimiter;
  3         40  
  3         97  
30 3     3   1280 use WebService::Async::CustomerIO::Trigger;
  3         8  
  3         161  
31              
32             use constant {
33 3         5641 TRACKING_END_POINT => 'https://track.customer.io/api/v1',
34             API_END_POINT => 'https://api.customer.io/v1',
35             RATE_LIMITS => {
36             track => {
37             limit => 30,
38             interval => 1
39             },
40             api => {
41             limit => 10,
42             interval => 1
43             },
44             trigger => {
45             limit => 1,
46             interval => 10
47             }, # https://www.customer.io/docs/api/#operation/triggerBroadcast
48 3     3   19 }};
  3         7  
49              
50             =head2 new
51              
52             Creates a new API client object
53              
54             Usage: C<< new(%params) -> obj >>
55              
56             Parameters:
57              
58             =over 4
59              
60             =item * C
61              
62             =item * C
63              
64             =item * C
65              
66             =back
67              
68             =cut
69              
70             sub _init {
71 26     26   57754 my ($self, $args) = @_;
72              
73 26         63 for my $k (qw(site_id api_key api_token)) {
74 72 100       794 Carp::croak "Missing required argument: $k" unless exists $args->{$k};
75 67 50       245 $self->{$k} = delete $args->{$k} if exists $args->{$k};
76             }
77              
78 21         83 return $self->next::method($args);
79             }
80              
81             sub configure {
82 21     21 1 344 my ($self, %args) = @_;
83              
84 21         46 for my $k (qw(site_id api_key api_token)) {
85 63 50       144 $self->{$k} = delete $args{$k} if exists $args{$k};
86             }
87              
88 21         56 return $self->next::method(%args);
89             }
90              
91             =head2 site_id
92              
93             =cut
94              
95 12     12 1 1038 sub site_id { return shift->{site_id} }
96              
97             =head2 api_key
98              
99             =cut
100              
101 12     12 1 107 sub api_key { return shift->{api_key} }
102              
103             =head2 api_token
104              
105             =cut
106              
107 3     3 1 18 sub api_token { return shift->{api_token} }
108              
109             =head2 API endpoints:
110              
111             There is 2 stable API for Customer.io, if you need to add a new method check
112             the L which endpoint
113             you need to use:
114              
115             =over 4
116              
117             =item * C - Behavioral Tracking API is used to identify and track
118             customer data with Customer.io.
119              
120             =item * C - Currently, this endpoint is used to fetch list of customers
121             given an email and for sending
122             L.
123              
124             =back
125              
126             =head2 tracking_request
127              
128             Sending request to Tracking API end point.
129              
130             Usage: C<< tracking_request($method, $uri, $data) -> future($data) >>
131              
132             =cut
133              
134             sub tracking_request {
135 1     1 1 398 my ($self, $method, $uri, $data) = @_;
136             return $self->ratelimiter('track')->acquire->then(
137             sub {
138 1     1   350 $self->_request($method, join(q{/} => (TRACKING_END_POINT, $uri)), $data);
139 1         4 });
140             }
141              
142             =head2 api_request
143              
144             Sending request to Regular API end point with optional limit type.
145              
146             Usage: C<< api_request($method, $uri, $data, $limit_type) -> future($data) >>
147              
148             =cut
149              
150             sub api_request {
151 1     1 1 766 my ($self, $method, $uri, $data, $limit_type) = @_;
152              
153 1 50       5 Carp::croak('API token is missed') unless $self->api_token;
154              
155             return $self->ratelimiter($limit_type // 'api')->acquire->then(
156             sub {
157 1     1   215 $self->_request($method, join(q{/} => (API_END_POINT, $uri)), $data, {authorization => 'Bearer ' . $self->api_token},);
158 1   50     14 });
159             }
160              
161             sub ratelimiter {
162 6     6 0 2498 my ($self, $type) = @_;
163              
164 6 100       29 return $self->{ratelimiters}{$type} if $self->{ratelimiters}{$type};
165              
166 3 50       9 Carp::croak "Can't use rate limiter without a loop" unless $self->loop;
167              
168 3         35 $self->{ratelimiters}{$type} = WebService::Async::CustomerIO::RateLimiter->new(RATE_LIMITS->{$type}->%*);
169              
170 3         95 $self->add_child($self->{ratelimiters}{$type});
171              
172 3         253 return $self->{ratelimiters}{$type};
173             }
174              
175             my %PATTERN_FOR_ERROR = (
176             RESOURCE_NOT_FOUND => qr/^404$/,
177             INVALID_REQUEST => qr/^400$/,
178             INVALID_API_KEY => qr/^401$/,
179             INTERNAL_SERVER_ERR => qr/^50[0234]$/,
180             );
181              
182             sub _request {
183 11     11   4865 my ($self, $method, $uri, $data, $headers) = @_;
184              
185 11 50       40 my $body =
    100          
186             $data ? encode_json_utf8($data)
187             : $method eq 'POST' ? q{}
188             : undef;
189              
190             return $self->_ua->do_request(
191             method => $method,
192             uri => $uri,
193             $headers->{authorization} ? ()
194             : (
195             user => $self->site_id,
196             pass => $self->api_key,
197             ),
198             !defined $body ? ()
199             : (
200             content => $body,
201             content_type => 'application/json',
202             ),
203             headers => $headers // {},
204              
205             )->catch(
206             sub {
207 8     8   1982 my ($code_msg, $err_type, $response) = @_;
208              
209 8 50 33     40 return Future->fail(@_) unless $err_type && $err_type eq 'http';
210              
211 8         47 my $code = $response->code;
212 8         541 my $request_data = {
213             method => $method,
214             uri => $uri,
215             data => $data
216             };
217              
218 8         31 for my $error_code (keys %PATTERN_FOR_ERROR) {
219 20 100       112 next unless $code =~ /$PATTERN_FOR_ERROR{$error_code}/;
220 7         30 return Future->fail($error_code, 'customerio', $request_data);
221             }
222              
223 1         6 return Future->fail('UNEXPECTED_HTTP_CODE: ' . $code_msg, 'customerio', $response);
224             }
225             )->then(
226             sub {
227 3     3   930 my ($response) = @_;
228             try {
229             my $response_data = decode_json_utf8($response->content);
230             return Future->done($response_data);
231 3         11 } catch {
232             return Future->fail('UNEXPECTED_RESPONSE_FORMAT', 'customerio', $@, $response);
233             }
234 11 50 50     40 });
    100          
235             }
236              
237             sub _ua {
238 0     0   0 my ($self) = @_;
239              
240 0 0       0 return $self->{ua} if $self->{ua};
241              
242 0         0 $self->{ua} = Net::Async::HTTP->new(
243             fail_on_error => 1,
244             decode_content => 0,
245             pipeline => 0,
246             stall_timeout => 60,
247             max_connections_per_host => 4,
248             user_agent => 'Mozilla/4.0 (WebService::Async::CustomerIO; BINARY@cpan.org; https://metacpan.org/pod/WebService::Async::CustomerIO)',
249             );
250              
251 0         0 $self->add_child($self->{ua});
252              
253 0         0 return $self->{ua};
254             }
255              
256             =head2 new_customer
257              
258             Creating new customer object
259              
260             Usage: C<< new_customer(%params) -> obj >>
261              
262             =cut
263              
264             sub new_customer {
265 0     0 1 0 my ($self, %param) = @_;
266              
267 0         0 return WebService::Async::CustomerIO::Customer->new(%param, api_client => $self);
268             }
269              
270             =head2 new_trigger
271              
272             Creating new trigger object
273              
274             Usage: C<< new_trigger(%params) -> obj >>
275              
276             =cut
277              
278             sub new_trigger {
279 0     0 1 0 my ($self, %param) = @_;
280              
281 0         0 return WebService::Async::CustomerIO::Trigger->new(%param, api_client => $self);
282             }
283              
284             =head2 new_customer
285              
286             Creating new customer object
287              
288             Usage: C<< new_customer(%params) -> obj >>
289              
290             =cut
291              
292             sub emit_event {
293 1     1 0 233 my ($self, %params) = @_;
294              
295 1         6 return $self->tracking_request(
296             POST => 'events',
297             \%params
298             );
299             }
300              
301             =head2 add_to_segment
302              
303             Add people to a manual segment.
304              
305             Usage: C<< add_to_segment($segment_id, @$customer_ids) -> Future() >>
306              
307             =cut
308              
309             sub add_to_segment {
310 3     3 1 3233 my ($self, $segment_id, $customers_ids) = @_;
311              
312 3 100       182 Carp::croak 'Missing required attribute: segment_id' unless $segment_id;
313 2 100       96 Carp::croak 'Invalid value for customers_ids' unless ref $customers_ids eq 'ARRAY';
314              
315 1         7 return $self->tracking_request(
316             POST => "segments/$segment_id/add_customers",
317             {ids => $customers_ids});
318             }
319              
320             =head2 remove_from_segment
321              
322             remove people from a manual segment.
323              
324             usage: c<< remove_from_segment($segment_id, @$customer_ids) -> future() >>
325              
326             =cut
327              
328             sub remove_from_segment {
329 3     3 1 3063 my ($self, $segment_id, $customers_ids) = @_;
330              
331 3 100       102 Carp::croak 'Missing required attribute: segment_id' unless $segment_id;
332 2 100       95 Carp::croak 'Invalid value for customers_ids' unless ref $customers_ids eq 'ARRAY';
333              
334 1         9 return $self->tracking_request(
335             POST => "segments/$segment_id/remove_customers",
336             {ids => $customers_ids});
337             }
338              
339             =head2 get_customers_by_email
340              
341             Query Customer.io API for list of clients, who has requested email address.
342              
343             usage: c<< get_customers_by_email($email)->future([$customer_obj1, ...]) >>
344              
345             =cut
346              
347             sub get_customers_by_email {
348 1     1 1 216 my ($self, $email) = @_;
349              
350 1 50       5 Carp::croak 'Missing required argument: email' unless $email;
351              
352             return $self->api_request(GET => "customers?email=" . uri_escape_utf8($email))->then(
353             sub {
354 1     1   279 my ($resp) = @_;
355              
356 1 50 33     13 if (ref $resp ne 'HASH' || ref $resp->{results} ne 'ARRAY') {
357 0         0 return Future->fail('UNEXPECTED_RESPONSE_FORMAT', 'customerio', 'Unexpected response format is recived', $resp);
358             }
359              
360             try {
361             my @customers = map { WebService::Async::CustomerIO::Customer->new($_->%*, api_client => $self) } $resp->{results}->@*;
362             return Future->done(\@customers);
363             } catch ($e) {
364             return Future->fail('UNEXPECTED_RESPONSE_FORMAT', 'customerio', $e, $resp);
365             }
366              
367 1         6 });
  1         5  
368             }
369              
370             1;