File Coverage

lib/WWW/Mixpanel.pm
Criterion Covered Total %
statement 16 81 19.7
branch 0 34 0.0
condition 0 12 0.0
subroutine 6 12 50.0
pod 3 3 100.0
total 25 142 17.6


line stmt bran cond sub pod time code
1             package WWW::Mixpanel;
2              
3 3     3   57816 use strict;
  3         4  
  3         90  
4 3     3   12 use warnings;
  3         3  
  3         69  
5 3     3   1855 use LWP::UserAgent;
  3         118118  
  3         108  
6 3     3   1739 use MIME::Base64;
  3         1978  
  3         222  
7 3     3   1959 use JSON;
  3         30160  
  3         14  
8              
9             BEGIN {
10 3     3   2535 $WWW::Mixpanel::VERSION = '0.04';
11             }
12              
13             sub new {
14 0     0 1   my ( $class, $token, $use_ssl, $api_key, $api_secret ) = @_;
15 0 0         die "You must provide your API token." unless $token;
16              
17 0           my $ua = LWP::UserAgent->new;
18 0           $ua->timeout(180);
19 0           $ua->env_proxy;
20              
21 0           my $json = JSON->new->allow_blessed(1)->convert_blessed(1);
22              
23 0           bless { token => $token,
24             use_ssl => $use_ssl,
25             api_key => $api_key,
26             api_secret => $api_secret,
27             data_api_default_expire_seconds => 180,
28             track_api => 'api.mixpanel.com/track/', # trailing slash required
29             data_api => 'mixpanel.com/api/2.0/',
30             json => $json,
31             ua => $ua, }, $class;
32             }
33              
34             sub track {
35 0     0 1   my ( $self, $event, %params ) = @_;
36              
37 0 0         die "You must provide an event name" unless $event;
38              
39 0   0       $params{time} ||= time();
40 0           $params{token} = $self->{token};
41              
42 0           my $data = { event => $event,
43             properties => \%params, };
44              
45 0 0         my $res =
46             $self->{ua}->post( $self->{use_ssl}
47             ? "https://$self->{track_api}"
48             : "http://$self->{track_api}",
49             { 'data' => encode_base64( $self->{json}->encode($data), '' ) } );
50              
51 0 0         if ( $res->is_success ) {
52 0 0         if ( $res->content == 1 ) {
53 0           return 1;
54             }
55             else {
56 0           die "Failure from api: " . $res->content;
57             }
58             }
59             else {
60 0           die "Failed sending event: " . $self->_res($res);
61             }
62             } # end track
63              
64             sub data {
65 0     0 1   my $self = shift;
66 0           my $methods = shift;
67 0           my %params = @_;
68              
69 0 0         $methods = [$methods] if !ref($methods);
70 0           my $api_methods = join( '/', @$methods );
71              
72 0           $self->_data_params_to_json( $api_methods, \%params );
73              
74 0   0       $params{format} ||= 'json';
75 0 0         $params{expire} = time() + $self->{data_api_default_expire_seconds}
76             if !defined( $params{expire} );
77 0   0       $params{api_key} = $self->{api_key} || die 'API Key must be specified for data requests';
78 0   0       my $api_secret = $self->{api_secret} || die 'API Secret must be specified for data requests';
79              
80 0           my $sig = $self->_create_sig( $api_secret, \%params );
81 0           $params{sig} = $sig;
82              
83 0 0         my $url =
84             $self->{use_ssl}
85             ? "https://$self->{data_api}"
86             : "http://$self->{data_api}";
87 0           $url .= $api_methods;
88              
89             # We have to hand-build the url because HTTP::REQUEST/HEADER was
90             # changing underscores and capitalization, and Mixpanel is sensitive
91             # about such things.
92 0           my $ps = join( '&', map {"$_=$params{$_}"} sort keys %params );
  0            
93 0           my $res = $self->{ua}->get( $url . '/?' . $ps );
94              
95 0 0         if ( $res->is_success ) {
96 0           my $reso = $res->content;
97 0 0         $reso = $self->{json}->decode($reso) if $params{format} eq 'json';
98 0           return $reso;
99             }
100             else {
101 0           die "Failed sending event: " . $self->_res($res);
102             }
103             } # end data
104              
105             # Calculate data request signature according to spec.
106             sub _create_sig {
107 0     0     my $self = shift;
108 0           my $api_secret = shift;
109 0           my $params = shift;
110              
111 0           require Digest::MD5;
112 0           my $pstr = join( '', map { $_ . '=' . $params->{$_} } sort keys %$params ) . $api_secret;
  0            
113 0           return Digest::MD5::md5_hex($pstr);
114             }
115              
116             sub _data_params_to_json {
117 0     0     my $self = shift;
118 0           my $api = shift;
119 0           my $params = shift;
120              
121             # A few API calls require json encoded arrays, so transform those here.
122 0           my $toj;
123 0 0         if ( $api eq 'events' ) {
124 0           $toj = 'event';
125             }
126 0 0         if ( $api eq 'events/properties' ) {
127 0           $toj = 'values';
128             }
129 0 0         if ( $api eq 'arb_funnels' ) {
130 0           $toj = 'events';
131             }
132              
133 0 0 0       if ( $toj && defined( $params->{$toj} ) ) {
134 0 0         $params->{$toj} = [ $params->{$toj} ] if !ref( $params->{$toj} );
135 0           $params->{$toj} = $self->{json}->encode( $params->{$toj} );
136             }
137              
138             } # end _data_params_to_json
139              
140             sub _res {
141 0     0     my ( $self, $res ) = @_;
142              
143 0 0         if ( $res->code == 500 ) {
    0          
144 0           return "Mixpanel service error. The service might be down.";
145             }
146             elsif ( $res->code == 400 ) {
147 0           return "Bad Request Elements: " . $res->content;
148             }
149             else {
150 0           return "Unknown error. " . $res->message;
151             }
152             }
153              
154             1;
155              
156              
157              
158             =pod
159              
160             =head1 NAME
161              
162             WWW::Mixpanel
163              
164             =head1 VERSION
165              
166             version 0.04
167              
168             =head1 SYNOPSIS
169              
170             use WWW::Mixpanel;
171             my $mp = WWW::Mixpanel->new( '1827378adad782983249287292a', 1 );
172             $mp->track('login', distinct_id => 'username', mp_name_tag => 'username', source => 'twitter');
173              
174             or if you also want to access the data api
175              
176             my $mp = WWW::Mixpanel->new(,1,,);
177             $mp->track('login', distinct_id => 'username', mp_name_tag => 'username', source => 'twitter');
178             my $enames = $mp->data( 'events/names', type => 'unique' );
179             my $fdates = $mp->data( 'funnels/dates',
180             funnel => [qw/funnel1 funnel2/],
181             unit => 'week' );
182              
183             =head1 DESCRIPTION
184              
185             The WWW::Mixpanel module is an implementation of the L API which provides realtime online analytics. L receives events from your application's perl code, javascript, email open and click tracking, and many more sources, and provides visualization and publishing of analytics.
186              
187             Currently, this module mirrors the event tracking API (L), and will be extended to include the powerful data access and platform parts of the api. B are always welcome, as are patches.
188              
189             This module is designed to die on failure, please use something like Try::Tiny.
190              
191             =head1 METHODS
192              
193             =head2 new( $token, [$use_ssl] )
194              
195             Returns a new instance of this class. You must supply the API token for your mixpanel project. HTTP is used to connect unless you provide a true value for use_ssl.
196              
197             =head2 track('', [time => timestamp, param => val, ...])
198              
199             Send an event to the API with the given event name, which is a required parameter. If you do not include a time parameter, the value of time() is set for you automatically. Other parameters are optional, and are included as-is as parameters in the api.
200              
201             This method returns 1 or dies with a message.
202              
203             Per the Mixpanel API, a 1 return indicates the event reached the mixpanel.com API and was properly formatted. 1 does not indicate the event was actually written to your project, in cases such as bad API token. This is a limitation of the service.
204              
205             You are strongly encouraged to use something like C to wrap calls to this API.
206              
207             Today, there is no way to set URL parameters such as ip=1, callback, img, redirect. You can supply ip as a parameter similar to distinct_id, to track users.
208              
209             =head2 data('', param => val, param => val ...)
210              
211             Obtain data from mixpanel.com using the L.
212             The first parameter to the method identifies the path off the api root.
213              
214             For example to access the C functionality, found at L, you would pass the string C to the data method.
215              
216             Some parameters of the data api are of array type, for example C parameter C. In every case where a parameter is of array type, you may supply the parameter as either an ARRAYREF or a single string.
217              
218             Unless specified as a parameter, the default return format is json.
219             This method will then return the result of the api call as a decoded perl object.
220              
221             If you specify format => 'csv', this method will return the csv return string unchanged.
222              
223             This method will die on errors, including malformed parameters, indicated by bad return codes from the api. It dies with the text of the api reply directly, often a json string indicating which parameter was malformed.
224              
225             I
226              
227             =head1 TODO
228              
229             =over 4
230              
231             =item /track to accept array of events
232              
233             Track will soon be able to accept many events, and will bulk-send them to mixpanel in one call if possible.
234              
235             =item /platform support
236              
237             The Platform API will be supported. Let me know if this is a feature you'd like to use.
238              
239             =back
240              
241             =head1 FEATURE REQUESTS
242              
243             Please send feature requests to me via rt or github. Patches are always welcome.
244              
245             =head1 BUGS
246              
247             Do your thing on CPAN.
248              
249             =head1 AFFILIATION
250              
251             I am not affiliated with mixpanel, I just use and like the service.
252              
253             =head1 AUTHOR
254              
255             Tom Eliaz
256              
257             =head1 COPYRIGHT AND LICENSE
258              
259             This software is copyright (c) 2012 by Tom Eliaz.
260              
261             This is free software; you can redistribute it and/or modify it under
262             the same terms as the Perl 5 programming language system itself.
263              
264             =cut
265              
266              
267             __END__