From eb844fd26e4a292f8a72cc6985f327f861d161ad Mon Sep 17 00:00:00 2001 From: Colin Kuskie Date: Wed, 21 Jan 2009 10:19:08 -0800 Subject: [PATCH] Add Passive Analytics modules, Workflow Activites, i18n and content handler. --- lib/WebGUI/Content/PassiveAnalytics.pm | 49 ++++ lib/WebGUI/PassiveAnalytics/Flow.pm | 254 ++++++++++++++++++ lib/WebGUI/PassiveAnalytics/Rule.pm | 92 +++++++ .../Activity/BucketPassiveAnalytics.pm | 138 ++++++++++ .../Activity/SummarizePassiveAnalytics.pm | 158 +++++++++++ lib/WebGUI/i18n/English/PassiveAnalytics.pm | 99 +++++++ 6 files changed, 790 insertions(+) create mode 100644 lib/WebGUI/Content/PassiveAnalytics.pm create mode 100644 lib/WebGUI/PassiveAnalytics/Flow.pm create mode 100644 lib/WebGUI/PassiveAnalytics/Rule.pm create mode 100644 lib/WebGUI/Workflow/Activity/BucketPassiveAnalytics.pm create mode 100644 lib/WebGUI/Workflow/Activity/SummarizePassiveAnalytics.pm create mode 100644 lib/WebGUI/i18n/English/PassiveAnalytics.pm diff --git a/lib/WebGUI/Content/PassiveAnalytics.pm b/lib/WebGUI/Content/PassiveAnalytics.pm new file mode 100644 index 000000000..7de3b86a8 --- /dev/null +++ b/lib/WebGUI/Content/PassiveAnalytics.pm @@ -0,0 +1,49 @@ +package WebGUI::Content::PassiveAnalytics; + +use strict; +use WebGUI::AdminConsole; +use WebGUI::Exception; +use WebGUI::PassiveAnalytics::Flow; + +=head1 NAME + +Package WebGUI::Content::PassiveAnalytics + +=head1 DESCRIPTION + +Handle all requests for building and editing Passive Analytic flows. + +=head1 SYNOPSIS + + use WebGUI::Content::PassiveAnalytics; + my $output = WebGUI::Content::PassiveAnalytics::handler($session); + +=head1 SUBROUTINES + +These subroutines are available from this package: + +=cut + +#------------------------------------------------------------------- + +=head2 handler ( session ) + +The content handler for this package. + +=cut + +sub handler { + my ($session) = @_; + my $output = undef; + return undef unless $session->form->get('op') eq 'passiveAnalytics'; + my $function = "www_".$session->form->get('func'); + if ($function ne "www_" && (my $sub = WebGUI::PassiveAnalytics::Flow->can($function))) { + $output = $sub->($session); + } + else { + WebGUI::Error::MethodNotFound->throw(error=>"Couldn't call non-existant method $function inside PassiveAnalytics", method=>$function); + } + return $output; +} + +1; diff --git a/lib/WebGUI/PassiveAnalytics/Flow.pm b/lib/WebGUI/PassiveAnalytics/Flow.pm new file mode 100644 index 000000000..143a24b66 --- /dev/null +++ b/lib/WebGUI/PassiveAnalytics/Flow.pm @@ -0,0 +1,254 @@ +package WebGUI::PassiveAnalytics::Flow; + +use strict; +use Tie::IxHash; +use WebGUI::AdminConsole; +use WebGUI::HTMLForm; +use WebGUI::International; +use WebGUI::Pluggable; +use WebGUI::PassiveAnalytics::Rule; +use WebGUI::Utility; +use WebGUI::HTMLForm; +use WebGUI::Workflow; +use WebGUI::Workflow::Instance; + +=head1 NAME + +Package WebGUI::PassiveAnalytics::Flow + +=head1 DESCRIPTION + +Web interface for making sets of rules for doing passive analytics, and +running them. + +=cut + +#---------------------------------------------------------------------------- + +=head2 canView ( session [, user] ) + +Returns true if the user can administrate this operation. user defaults to +the current user. + +=cut + +sub canView { + my $session = shift; + my $user = shift || $session->user; + return $user->isInGroup( 3 ); +} + +#------------------------------------------------------------------- + +=head2 www_deleteRule ( ) + +Deletes an activity from a workflow. + +=cut + +sub www_deleteRule { + my $session = shift; + return $session->privilege->insufficient() unless canView($session); + my $rule = WebGUI::PassiveAnalytics::Rule->new($session, $session->form->get("ruleId")); + if (defined $rule) { + $rule->delete; + } + return www_editRuleflow($session); +} + +#------------------------------------------------------------------ + +=head2 www_demoteRule ( session ) + +Moves a Rule down one position in the execution order. + +=head3 session + +A reference to the current session. + +=cut + +sub www_demoteRule { + my $session = shift; + return $session->privilege->insufficient() unless canView($session); + my $rule = WebGUI::PassiveAnalytics::Rule->new($session, $session->form->get("ruleId")); + if (defined $rule) { + $rule->demote; + } + return www_editRuleflow($session); +} + +#------------------------------------------------------------------- + +=head2 www_editRuleflow ( session ) + +Configure a set of analyses to run on the passive logs. The analysis is destructive. + +=cut + +sub www_editRuleflow { + my $session = shift; + my $error = shift; + return $session->privilege->insufficient() unless canView($session); + if ($error) { + $error = qq|
$error
\n|; + } + my $i18n = WebGUI::International->new($session, "PassiveAnalytics"); + my $addmenu = '
'; + $addmenu .= sprintf '%s', + $session->url->page('op=passiveAnalytics;func=editRule'), + $i18n->get('Add a bucket'); + $addmenu .= '
'; + my $f = WebGUI::HTMLForm->new($session); + $f->hidden( + name=>'op', + value=>'passiveAnalytics' + ); + $f->hidden( + name=>'func', + value=>'editRuleflowSave' + ); + $f->integer( + name => 'pauseInterval', + value => 300, + label => $i18n->get('pause interval'), + hoverHelp => $i18n->get('pause interval help'), + ); + $f->submit(value => $i18n->get('Begin analysis')); + my $steps = ''; + my $getARule = WebGUI::PassiveAnalytics::Rule->getAllIterator($session); + my $icon = $session->icon; + while (my $rule = $getARule->()) { + my $id = $rule->getId; + my $bucket = $rule->get('bucketName'); + $steps .= ''; + + } + $steps .= '
' + . $icon->delete( 'op=passiveAnalytics;func=deleteRule;ruleId='.$id, undef, $i18n->get('confirm delete rule')) + . $icon->edit( 'op=passiveAnalytics;func=editRule;ruleId='.$id) + . $icon->moveDown('op=passiveAnalytics;func=demoteRule;ruleId='.$id) + . $icon->moveUp( 'op=passiveAnalytics;func=promoteRule;ruleId='.$id) + . ''.$bucket.'
 Other
'; + my $ac = WebGUI::AdminConsole->new($session,'passiveAnalytics'); + return $ac->render($error.$f->print.$addmenu.$steps, 'Passive Analytics'); +} + +#------------------------------------------------------------------- + +=head2 www_editRuleflowSave ( ) + +Saves the results of www_editRuleflow() + +=cut + +sub www_editRuleflowSave { + my $session = shift; + return $session->privilege->insufficient() unless canView($session); + my $workflow = WebGUI::Workflow->new($session, 'PassiveAnalytics000001'); + return www_editRuleflow($session, "The Passive Analytics workflow has been deleted. Please contact an Administrator immediately.") unless defined $workflow; + my $delta = $session->form->process('pauseInterval','integer'); + my $activities = $workflow->getActivities(); + ##Note, they're in order, and the order is known. + $activities->[0]->set('deltaInterval', $delta); + $activities->[1]->set('userId', $session->user->userId); + my $instance = WebGUI::Workflow::Instance->create($session, { + workflowId => $workflow->getId, + priority => 1, + }); + if (!defined $instance) { + return www_editRuleflow($session, "A Passive Analytics analysis is currently running.") if $session->stow->get('singletonWorkflowClash'); + return www_editRuleflow($session, "Error creating the workflow instance."); + } + $instance->start('skipRealtime'); + return www_editRuleflow($session, "Passive Analytics session started"); +} + + +#------------------------------------------------------------------- + +=head2 www_editRule ( ) + +Displays a form to edit the properties rule. + +=cut + +sub www_editRule { + my $session = shift; + return $session->privilege->insufficient() unless canView($session); + + ##Make a PassiveAnalytics rule to use to populate the form. + my $ruleId = $session->form->get('ruleId'); + my $rule; + if ($ruleId) { + $rule = WebGUI::PassiveAnalytics::Rule->new($session, $ruleId); + } + else { + ##We need a temporary rule so that we can call dynamicForm, below + $ruleId = 'new'; + $rule = WebGUI::PassiveAnalytics::Rule->create($session, {}); + } + + ##Build the form + my $form = WebGUI::HTMLForm->new($session); + $form->hidden( name=>"op", value=>"passiveAnalytics"); + $form->hidden( name=>"func", value=>"editRuleSave"); + $form->hidden( name=>"ruleId", value=>$ruleId); + $form->dynamicForm([WebGUI::PassiveAnalytics::Rule->crud_definition($session)], 'properties', $rule); + $form->submit; + + my $i18n = WebGUI::International->new($session, 'PassiveAnalytics'); + my $ac = WebGUI::AdminConsole->new($session,'passiveAnalytics'); + $ac->addSubmenuItem($session->url->page("op=passiveAnalytics;func=editRuleflow"), $i18n->get("manage ruleset")); + if ($ruleId eq 'new') { + $rule->delete; + } + return $ac->render($form->print,$i18n->get('Edit Rule')); +} + +#------------------------------------------------------------------- + +=head2 www_editRuleSave ( ) + +Saves the results of www_editRule(). + +=cut + +sub www_editRuleSave { + my $session = shift; + return $session->privilege->insufficient() unless canView($session); + my $ruleId = $session->form->get('ruleId'); + my $rule; + if ($ruleId eq 'new') { + $rule = WebGUI::PassiveAnalytics::Rule->create($session, {}); + } + else { + $rule = WebGUI::PassiveAnalytics::Rule->new($session, $ruleId); + } + $rule->updateFromFormPost if $rule; + return www_editRuleflow($session); +} + +#------------------------------------------------------------------ + +=head2 www_promoteRule ( session ) + +Moves a rule up one position in the execution order. + +=head3 session + +A reference to the current session. + +=cut + +sub www_promoteRule { + my $session = shift; + return $session->privilege->insufficient() unless canView($session); + my $rule = WebGUI::PassiveAnalytics::Rule->new($session, $session->form->get("ruleId")); + if (defined $rule) { + $rule->promote; + } + return www_editRuleflow($session); +} + +1; diff --git a/lib/WebGUI/PassiveAnalytics/Rule.pm b/lib/WebGUI/PassiveAnalytics/Rule.pm new file mode 100644 index 000000000..5d052c8da --- /dev/null +++ b/lib/WebGUI/PassiveAnalytics/Rule.pm @@ -0,0 +1,92 @@ +package WebGUI::PassiveAnalytics::Rule; + +use base qw/WebGUI::Crud/; +use WebGUI::International; + +=head1 NAME + +Package WebGUI::PassiveAnalytics::Rule; + +=head1 DESCRIPTION + +Base class for rules that are used to analyze the Passive Analytics log. + +=head1 METHODS + +These methods are available from this class: + +=cut + +#------------------------------------------------------------------- + +=head2 crud_definition ( ) + +WebGUI::Crud definition for this class. + +=head3 tableName + +analyticRule. + +=head3 tableKey + +ruleId + +=head3 sequenceKey + +None. There is only 1 sequence of rules for a site. + +=head3 properties + +=head4 bucketName + +The name of a bucket to hold results for this rule. + +=head4 rules + +JSON blob with configuration data for the individual rules. + +=cut + +sub crud_definition { + my ($class, $session) = @_; + my $definition = $class->SUPER::crud_definition($session); + $definition->{tableName} = 'analyticRule'; + $definition->{tableKey} = 'ruleId'; + $definition->{sequenceKey} = ''; + my $properties = $definition->{properties}; + my $i18n = WebGUI::International->new($session); + $properties->{bucketName} = { + fieldType => 'text', + label => $i18n->get('Bucket Name','PassiveAnalytics'), + hoverHelp => $i18n->get('Bucket Name help','PassiveAnalytics'), + defaultValue => '', + }; + $properties->{regexp} = { + fieldType => 'text', + label => $i18n->get('regexp','PassiveAnalytics'), + hoverHelp => $i18n->get('regexp help','PassiveAnalytics'), + defaultValue => '.+', + }; + return $definition; +} + +#------------------------------------------------------------------- + +=head2 matchesBucket ( $logLine ) + +Executes the rule to determine if a log file entry matches the rule. + +=head3 $logLine + +A hashref of information from 1 line of the logs. + +=cut + +sub matchesBucket { + my ($self, $logLine) = @_; + my $regexp = $self->get('regexp'); + return $logLine->{url} =~ m/$regexp/; +} + +1; +#vim:ft=perl diff --git a/lib/WebGUI/Workflow/Activity/BucketPassiveAnalytics.pm b/lib/WebGUI/Workflow/Activity/BucketPassiveAnalytics.pm new file mode 100644 index 000000000..170d50dfb --- /dev/null +++ b/lib/WebGUI/Workflow/Activity/BucketPassiveAnalytics.pm @@ -0,0 +1,138 @@ +package WebGUI::Workflow::Activity::BucketPassiveAnalytics; + + +=head1 LEGAL + + ------------------------------------------------------------------- + Copyright 2001-2008 SDH Corporation + ------------------------------------------------------------------- + +=cut + +use strict; +use base 'WebGUI::Workflow::Activity'; +use WebGUI::PassiveAnalytics::Rule; +use WebGUI::Inbox; + +=head1 NAME + +Package WebGUI::Workflow::Activity::BucketPassiveAnalytics + +=head1 DESCRIPTION + +Run through a set of rules to figure out how to classify log file entries. + +=head1 SYNOPSIS + +See WebGUI::Workflow::Activity for details on how to use any activity. + +=head1 METHODS + +These methods are available from this class: + +=cut + + +#------------------------------------------------------------------- + +=head2 definition ( session, definition ) + +See WebGUI::Workflow::Activity::defintion() for details. + +=cut + +sub definition { + my $class = shift; + my $session = shift; + my $definition = shift; + my $i18n = WebGUI::International->new($session, "PassiveAnalytics"); + push( @{$definition}, { + name=>$i18n->get("Bucket Passive Analytics"), + properties=> { + notifyUser => { + fieldType => 'user', + label => $i18n->get('User'), + hoverHelp => $i18n->get('User help'), + defaultValue => $session->user->userId, + }, + }, + }); + return $class->SUPER::definition($session,$definition); +} + + +#------------------------------------------------------------------- + +=head2 execute ( [ object ] ) + +Analyze the deltaLog table, and generate the bucketLog table. + +=head3 notes + +=cut + +sub execute { + my ($self, undef, $instance) = @_; + my $session = $self->session; + my $endTime = time() + $self->getTTL; + my $expired = 0; + + ##Load all the rules into an array + my @rules = (); + my $getARule = WebGUI::PassiveAnalytics::Rule->getAllIterator($session); + while (my $rule = $getARule->()) { + push @rules, $rule; + } + + ##Get the index stored from the last invocation of the Activity. If this is + ##the first run, then clear out the table. + my $logIndex = $instance->getScratch('lastPassiveLogIndex') || 0; + if ($logIndex == 0) { + $session->db->write('delete from bucketLog'); + } + + ##Configure all the SQL + my $deltaSql = <<"EOSQL1"; +select userId, assetId, url, delta, from_unixtime(timeStamp) as stamp + from deltaLog order by timestamp limit $logIndex, 1234567890 +EOSQL1 + my $deltaSth = $session->db->read($deltaSql); + my $bucketSth = $session->db->prepare('insert into bucketLog (userId, Bucket, duration, timeStamp) VALUES (?,?,?,?)'); + + ##Walk through the log file entries, one by one. Run each entry against + ##all the rules until 1 matches. If it doesn't match any rule, then bin it + ##into the "Other" bucket. + DELTA_ENTRY: while (my $entry = $deltaSth->hashRef()) { + ++$logIndex; + my $bucketFound = 0; + RULE: foreach my $rule (@rules) { + next RULE unless $rule->matchesBucket($entry); + $bucketSth->execute([$entry->{userId}, $rule->get('bucketName'), $entry->{delta}, $entry->{stamp}]); + } + if (!$bucketFound) { + $bucketSth->execute([$entry->{userId}, 'Other', $entry->{delta}, $entry->{stamp}]); + } + if (time() > $endTime) { + $expired = 1; + last DELTA_ENTRY; + } + } + + if ($expired) { + $instance->setScratch('logIndex', $logIndex); + return $self->WAITING(1); + } + my $inbox = WebGUI::Inbox->new($self->session); + $inbox->addMessage({ + status => 'unread', + subject => 'Passive analytics is done', + userId => $self->get('userId'), + message => 'Passive analytics is done', + }); + + return $self->COMPLETE; +} + +1; + +#vim:ft=perl diff --git a/lib/WebGUI/Workflow/Activity/SummarizePassiveAnalytics.pm b/lib/WebGUI/Workflow/Activity/SummarizePassiveAnalytics.pm new file mode 100644 index 000000000..7d5e52f49 --- /dev/null +++ b/lib/WebGUI/Workflow/Activity/SummarizePassiveAnalytics.pm @@ -0,0 +1,158 @@ +package WebGUI::Workflow::Activity::SummarizePassiveAnalytics; + + +=head1 LEGAL + + ------------------------------------------------------------------- + Copyright 2001-2008 SDH Corporation + ------------------------------------------------------------------- + +=cut + +use strict; +use base 'WebGUI::Workflow::Activity'; + +=head1 NAME + +Package WebGUI::Workflow::Activity::SummarizePassiveAnalytics + +=head1 DESCRIPTION + +Summarize how long a user stayed on a page, using a user supplied interval. + +=head1 SYNOPSIS + +See WebGUI::Workflow::Activity for details on how to use any activity. + +=head1 METHODS + +These methods are available from this class: + +=cut + + +#------------------------------------------------------------------- + +=head2 definition ( session, definition ) + +See WebGUI::Workflow::Activity::defintion() for details. + +=cut + +sub definition { + my $class = shift; + my $session = shift; + my $definition = shift; + my $i18n = WebGUI::International->new($session, 'PassiveAnalytics'); + push(@{$definition}, { + name=>$i18n->get('activityName'), + properties=> { + deltaInterval => { + fieldType => 'interval', + label => $i18n->get('pause interval'), + defaultValue => 15, + hoverHelp => $i18n->get('pause interval help'), + }, + } + }); + return $class->SUPER::definition($session,$definition); +} + + +#------------------------------------------------------------------- + +=head2 execute ( [ object ] ) + +Analyze the passiveLog table, and generate the deltaLog table. + +=head3 notes + +If there is only 1 line in the table for a particular sessionId or +userId, no conclusions as to how long the user viewed a page can be +drawn from that. Similarly, the last entry in their browsing log +yields no data, since we require another entry in the passiveLog to +determine a delta. + +=cut + +sub execute { + my ($self, undef, $instance) = @_; + my $session = $self->session; + my $endTime = time() + $self->getTTL; + my $deltaInterval = $self->get('deltaInterval'); + + my $passive = 'select * from passiveLog where userId <> 1 order by userId, sessionId, timeStamp'; + my $sth; + my $lastUserId; + my $lastSessionId; + my $lastTimeStamp; + my $lastAssetId; + my $lastUrl; + my $counter = $instance->getScratch('counter'); + if ($counter) { + $passive .= ' limit '. $counter .', 1234567890'; + $sth = $session->db->read($passive); + $lastUserId = $instance->getScratch('lastUserId'); + $lastSessionId = $instance->getScratch('lastSessionId'); + $lastTimeStamp = $instance->getScratch('lastTimeStamp'); + $lastAssetId = $instance->getScratch('lastAssetId'); + $lastUrl = $instance->getScratch('lastUrl'); + } + else { + $sth = $session->db->read($passive); + my $logLine = $sth->hashRef(); + $logLine = $sth->hashRef(); + $lastUserId = $logLine->{userId}; + $lastSessionId = $logLine->{sessionId}; + $lastTimeStamp = $logLine->{timeStamp}; + $lastAssetId = $logLine->{assetId}; + $lastUrl = $logLine->{url}; + } + + $session->db->write('delete from deltaLog'); ##Only if we're starting out + my $deltaLog = $session->db->prepare('insert into deltaLog (userId, assetId, delta, timeStamp, url) VALUES (?,?,?,?,?)'); + + my $expired = 0; + LOG_ENTRY: while (my $logLine = $sth->hashRef()) { + $counter++; + my $delta = $logLine->{timeStamp} - $lastTimeStamp; + if ( $logLine->{userId} eq $lastUserId + && $logLine->{sessionId} eq $lastSessionId + && $delta < $deltaInterval ) { + $deltaLog->execute([$lastUserId, $lastAssetId, $delta, $lastTimeStamp, $lastUrl]); + } + $lastUserId = $logLine->{userId}; + $lastSessionId = $logLine->{sessionId}; + $lastTimeStamp = $logLine->{timeStamp}; + $lastAssetId = $logLine->{assetId}; + $lastUrl = $logLine->{url}; + if (time() > $endTime) { + $instance->setScratch('lastUserId', $lastUserId); + $instance->setScratch('lastSessionId', $lastSessionId); + $instance->setScratch('lastTimeStamp', $lastTimeStamp); + $instance->setScratch('lastAssetId', $lastAssetId); + $instance->setScratch('lastUrl', $lastUrl); + $instance->setScratch('counter', $counter); + $expired = 1; + last LOG_ENTRY; + } + } + + if ($expired) { + return $self->WAITING(1); + } + + $instance->deleteScratch('lastUserId'); + $instance->deleteScratch('lastSessionId'); + $instance->deleteScratch('lastTimeStamp'); + $instance->deleteScratch('lastAssetId'); + $instance->deleteScratch('lastUrl'); + $instance->deleteScratch('counter'); + return $self->COMPLETE; +} + + + +1; + +#vim:ft=perl diff --git a/lib/WebGUI/i18n/English/PassiveAnalytics.pm b/lib/WebGUI/i18n/English/PassiveAnalytics.pm new file mode 100644 index 000000000..b46a12eca --- /dev/null +++ b/lib/WebGUI/i18n/English/PassiveAnalytics.pm @@ -0,0 +1,99 @@ +package WebGUI::i18n::English::PassiveAnalytics; + +use strict; + +our $I18N = { + + 'Summarize Passive Analytics' => { + message => q|Summarize Passive Analytics|, + context => q|The name of this workflow activity.|, + lastUpdated => 0, + }, + + 'pause interval' => { + message => q|Pause threshold|, + lastUpdated => 0, + }, + + 'pause interval help' => { + message => q|Set the time between clicks that is interpreted as the user reading the page, as opposed to beginning a new browsing session, or leaving the site.|, + lastUpdated => 0, + }, + + 'other' => { + message => q|Other|, + lastUpdated => 0, + context => q|Meaning not like anything in a set. This, that and the other one. Also, a catch all.| + }, + + 'Bucket Name' => { + message => q|Bucket Name|, + lastUpdated => 0, + context => q|To name a container, or bucket.| + }, + + 'Bucket Name help' => { + message => q|Pick a unique, descriptive short name for this bucket.|, + lastUpdated => 0, + context => q|| + }, + + 'regexp' => { + message => q|Regular expression|, + lastUpdated => 0, + context => q|| + }, + + 'regexp help' => { + message => q|Define a regular expression to pick log entries for this bucket.
+^ = beginning of url
+$ = end of url
+. = any character
+* = any amount
++ = 1 or more
+? = 0 or 1
+|, + lastUpdated => 0, + context => q|| + }, + + 'Passive Analytics' => { + message => q|Passive Analytics|, + lastUpdated => 0, + context => q|| + }, + + 'Edit Rule' => { + message => q|Edit Rule|, + lastUpdated => 0, + context => q|| + }, + + 'Add a bucket' => { + message => q|Add a bucket|, + lastUpdated => 0, + context => q|| + }, + + 'User' => { + message => q|User|, + lastUpdated => 0, + context => q|| + }, + + 'User help' => { + message => q|The user who will recieve an email when bucket processing is done.|, + lastUpdated => 0, + context => q|| + }, + + 'Begin analysis' => { + message => q|Begin analysis|, + lastUpdated => 0, + context => q|Button label to begin analyzing the logs.| + }, + +}; + +1; +#vim:ft=perl