File Coverage

blib/lib/WebService/Async/CustomerIO.pm
Criterion Covered Total %
statement 103 113 91.1
branch 30 42 71.4
condition 8 20 40.0
subroutine 35 38 92.1
pod 14 16 87.5
total 190 229 82.9


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