jenkins-bot has submitted this change and it was merged.

Change subject: EventLog Flow topic and post actions (FlowReplies)
......................................................................


EventLog Flow topic and post actions (FlowReplies)

This will track all events defined in 
https://meta.wikimedia.org/wiki/Schema:FlowReplies

It works by setting some data-attributes on nodes that you want to track (on
click). Those attributes are:
* data-flow-eventlog-schema: name of the schema
* data-flow-eventlog-entrypoint: name of the entrypoint param
* data-flow-eventlog-action: name of the action param
* data-flow-eventlog-forward: selectors to nodes to forward to

This forwarding is because we track nodes in a funnel. Most properties for
subsequent calls in a funnel are the same - only action will be different.
For follow-up nodes to be tracked in a funnel, the only data-attribute that is
needed is data-flow-eventlog-action (which is what the clickhandler is bound to)

Since things in Flow happen through a variety of ways, we can't use the click
handler for everything we want to log:
* opening a new-topic or reply-bottom form is via a focusin event
* cancel-attempt, cancel-abort & cancel-success are done by JS
* preview & keep-editing are done by JS

I've added a way to add interactiveHandlers on focus (patch prior to this) so
we can deal with the former. The latter is solved by directly calling the log
code from JS. This way, this eventlogging stuff is at least still *mostly*
driven by data attributes.

I've also made all interactiveHandlers & apiHandlers (patch prior to this)
return deferred objects, which only resolve (or reject) when all of the work
is really done. That way, we can delay the forwarding until we're certain the
nodes will have already been rendered (that's async)

Change-Id: Ic011a6130184aadae539eced307bf982f84c990d
---
M Flow.php
M Resources.php
M handlebars/compiled/flow_block_topic.handlebars.php
M handlebars/compiled/flow_block_topic_moderate_post.handlebars.php
M handlebars/compiled/flow_block_topic_moderate_topic.handlebars.php
M handlebars/compiled/flow_block_topiclist.handlebars.php
M handlebars/compiled/flow_block_topiclist_newtopic.handlebars.php
M handlebars/compiled/flow_block_topicsummary_edit.handlebars.php
M handlebars/compiled/flow_form_buttons.handlebars.php
M handlebars/compiled/flow_post.handlebars.php
M handlebars/flow_form_buttons.handlebars
M handlebars/flow_newtopic_form.handlebars
M handlebars/flow_post_meta_actions.handlebars
M handlebars/flow_reply_form.handlebars
M handlebars/flow_topic_titlebar_content.handlebars
M modules/engine/components/board/base/flow-board-api-events.js
M modules/engine/components/board/base/flow-board-interactive-events.js
M modules/engine/components/board/base/flow-board-misc.js
M modules/engine/components/board/base/flow-boardandhistory-base.js
M modules/engine/components/common/flow-component-engines.js
M modules/engine/components/common/flow-component-events.js
A modules/engine/misc/flow-eventlog.js
22 files changed, 598 insertions(+), 59 deletions(-)

Approvals:
  EBernhardson: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/Flow.php b/Flow.php
index f03258d..8c68f64 100644
--- a/Flow.php
+++ b/Flow.php
@@ -287,3 +287,6 @@
 // of sending Cookie headers to Parsoid over HTTP. For security reasons, it is 
strongly recommended
 // that $wgVisualEditorParsoidURL be pointed to localhost if this setting is 
enabled.
 $wgFlowParsoidForwardCookies = false;
+
+// Enable/disable event logging
+$wgFlowEventLogging = true;
diff --git a/Resources.php b/Resources.php
index 8a73256..7cbd613 100644
--- a/Resources.php
+++ b/Resources.php
@@ -319,6 +319,8 @@
                        'engine/misc/mw-ui.modal.js',
                        // FlowApi
                        'engine/misc/flow-api.js',
+                       // FlowEventLog
+                       'engine/misc/flow-eventlog.js',
                        // Component registry
                        'engine/components/flow-registry.js',
                        // FlowComponent must come before actual components
@@ -402,3 +404,22 @@
                ),
        ) + $mobile,
 );
+
+$wgHooks['ResourceLoaderRegisterModules'][] = function ( ResourceLoader 
&$resourceLoader ) {
+       global $wgFlowEventLogging, $wgResourceModules;
+
+       // Only if EventLogging in Flow is enabled & EventLogging exists
+       if ( $wgFlowEventLogging && class_exists( 'ResourceLoaderSchemaModule' 
) ) {
+               $resourceLoader->register( 'schema.FlowReplies', array(
+                       'class' => 'ResourceLoaderSchemaModule',
+                       'schema' => 'FlowReplies',
+                       // See 
https://meta.wikimedia.org/wiki/Schema:FlowReplies, below title
+                       'revision' => 10561344,
+               ) );
+
+               // Add as dependency to Flow JS
+               $wgResourceModules['ext.flow']['dependencies'][] = 
'schema.FlowReplies';
+       }
+
+       return true;
+};
diff --git a/handlebars/compiled/flow_block_topic.handlebars.php 
b/handlebars/compiled/flow_block_topic.handlebars.php
index 9c7540d..f691efa 100644
--- a/handlebars/compiled/flow_block_topic.handlebars.php
+++ b/handlebars/compiled/flow_block_topic.handlebars.php
@@ -57,6 +57,16 @@
                   
title="'.htmlentities((string)((isset($in['actions']['reply']['title']) && 
is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), 
ENT_QUOTES, 'UTF-8').'"
                   class="mw-ui-anchor mw-ui-progressive mw-ui-quiet"
                   data-flow-interactive-handler="activateForm"
+
+                  
+                  data-flow-eventlog-schema="FlowReplies"
+                  data-flow-eventlog-action="initiate"
+                  data-flow-eventlog-entrypoint="reply-top"
+                  data-flow-eventlog-forward="
+                      < .flow-topic .flow-reply-form:last 
[data-role=\'cancel\'],
+                      < .flow-topic .flow-reply-form:last 
[data-role=\'action\'][name=\'preview\'],
+                      < .flow-topic .flow-reply-form:last 
[data-role=\'submit\']
+                  "
                
>'.htmlentities((string)((isset($in['actions']['reply']['title']) && 
is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), 
ENT_QUOTES, 'UTF-8').'</a>
                &bull;
        ' : '').'
@@ -88,7 +98,6 @@
           class="mw-ui-anchor mw-ui-constructive '.((!LCRun3::ifvar($cx, 
((isset($in['isWatched']) && is_array($in)) ? $in['isWatched'] : null))) ? 
'mw-ui-quiet' : '').'
           '.((LCRun3::ifvar($cx, ((isset($in['isWatched']) && is_array($in)) ? 
$in['isWatched'] : null))) ? 'flow-watch-link-unwatch' : 
'flow-watch-link-watch').'"
           data-flow-api-handler="watchItem"
-          data-flow-api-handler="watchTopic"
           data-flow-api-target="< .flow-topic-watchlist"
           
data-flow-api-method="POST">'.htmlentities((string)((isset($in['null']) && 
is_array($in)) ? $in['null'] : null), ENT_QUOTES, 'UTF-8').'<span 
class="wikiglyph 
wikiglyph-star"></span>'.htmlentities((string)((isset($in['null']) && 
is_array($in)) ? $in['null'] : null), ENT_QUOTES, 
'UTF-8').''.htmlentities((string)((isset($in['null']) && is_array($in)) ? 
$in['null'] : null), ENT_QUOTES, 'UTF-8').'<span class="wikiglyph 
wikiglyph-unstar"></span>'.htmlentities((string)((isset($in['null']) && 
is_array($in)) ? $in['null'] : null), ENT_QUOTES, 'UTF-8').'</a>
 </div>
@@ -203,12 +212,17 @@
         name="preview"
         data-role="action"
         class="mw-ui-button mw-ui-progressive mw-ui-quiet mw-ui-flush-right 
flow-js"
+
+        
 >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-preview'),Array()), 
 >'encq').'</button>
 
 <button data-flow-interactive-handler="cancelForm"
         data-role="cancel"
         type="reset"
         class="mw-ui-button mw-ui-destructive mw-ui-quiet mw-ui-flush-right 
flow-js"
+
+        
+        
 >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-cancel'),Array()), 
 >'encq').'</button>
 ';},'flow_reply_form' => function ($cx, $in) {return ''.((LCRun3::ifvar($cx, 
((isset($in['actions']['reply']) && is_array($in['actions'])) ? 
$in['actions']['reply'] : null))) ? '
        <form class="flow-post flow-reply-form"
@@ -230,17 +244,23 @@
                                required
                                data-flow-preview-template="flow_post"
                                data-flow-expandable="true"
-                               class="mw-ui-input"
+                               class="mw-ui-input flow-click-interactive"
                                type="text"
                                placeholder="'.LCRun3::ch($cx, 'l10n', 
Array(Array('flow-reply-topic-title-placeholder',((isset($in['properties']['topic-of-post'])
 && is_array($in['properties'])) ? $in['properties']['topic-of-post'] : 
null)),Array()), 'encq').'"
-                               data-role="content">'.((LCRun3::ifvar($cx, 
((isset($cx['scopes'][0]['submitted']) && is_array($cx['scopes'][0])) ? 
$cx['scopes'][0]['submitted'] : null))) ? ''.LCRun3::hbch($cx, 'ifCond', 
Array(Array(((isset($cx['scopes'][0]['submitted']['postId']) && 
is_array($cx['scopes'][0]['submitted'])) ? 
$cx['scopes'][0]['submitted']['postId'] : null),'===',((isset($in['postId']) && 
is_array($in)) ? $in['postId'] : null)),Array()), $in, function($cx, $in) 
{return 
''.htmlentities((string)((isset($cx['scopes'][0]['submitted']['content']) && 
is_array($cx['scopes'][0]['submitted'])) ? 
$cx['scopes'][0]['submitted']['content'] : null), ENT_QUOTES, 'UTF-8').'';}).'' 
: '').'</textarea>
+                               data-role="content"
+
+                               
+                               
data-flow-interactive-handler-focus="activateReplyTopic"
+               >'.((LCRun3::ifvar($cx, ((isset($cx['scopes'][0]['submitted']) 
&& is_array($cx['scopes'][0])) ? $cx['scopes'][0]['submitted'] : null))) ? 
''.LCRun3::hbch($cx, 'ifCond', 
Array(Array(((isset($cx['scopes'][0]['submitted']['postId']) && 
is_array($cx['scopes'][0]['submitted'])) ? 
$cx['scopes'][0]['submitted']['postId'] : null),'===',((isset($in['postId']) && 
is_array($in)) ? $in['postId'] : null)),Array()), $in, function($cx, $in) 
{return 
''.htmlentities((string)((isset($cx['scopes'][0]['submitted']['content']) && 
is_array($cx['scopes'][0]['submitted'])) ? 
$cx['scopes'][0]['submitted']['content'] : null), ENT_QUOTES, 'UTF-8').'';}).'' 
: '').'</textarea>
 
                <div class="flow-form-actions flow-form-collapsible">
                        <button data-role="submit"
                                class="mw-ui-button mw-ui-constructive"
                                data-flow-interactive-handler="apiRequest"
                                data-flow-api-handler="submitReply"
-                               data-flow-api-target="< 
.flow-topic">'.htmlentities((string)((isset($in['actions']['reply']['title']) 
&& is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : 
null), ENT_QUOTES, 'UTF-8').'</button>
+                               data-flow-api-target="< .flow-topic"
+                               data-flow-eventlog-action="save-attempt"
+                       
>'.htmlentities((string)((isset($in['actions']['reply']['title']) && 
is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), 
ENT_QUOTES, 'UTF-8').'</button>
                        '.LCRun3::p($cx, 'flow_form_buttons', 
Array(Array($in),Array())).'
                        <small class="flow-terms-of-use 
plainlinks">'.LCRun3::ch($cx, 'l10nParse', 
Array(Array('flow-terms-of-use-reply'),Array()), 'encq').'</small>
                </div>
@@ -304,4 +324,4 @@
 </div>
 ';
 }
-?>
+?>
\ No newline at end of file
diff --git a/handlebars/compiled/flow_block_topic_moderate_post.handlebars.php 
b/handlebars/compiled/flow_block_topic_moderate_post.handlebars.php
index 36bb397..7748d0b 100644
--- a/handlebars/compiled/flow_block_topic_moderate_post.handlebars.php
+++ b/handlebars/compiled/flow_block_topic_moderate_post.handlebars.php
@@ -84,7 +84,18 @@
                        <a 
href="'.htmlentities((string)((isset($in['actions']['reply']['url']) && 
is_array($in['actions']['reply'])) ? $in['actions']['reply']['url'] : null), 
ENT_QUOTES, 'UTF-8').'"
                           
title="'.htmlentities((string)((isset($in['actions']['reply']['title']) && 
is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), 
ENT_QUOTES, 'UTF-8').'"
                           class="mw-ui-anchor mw-ui-progressive mw-ui-quiet"
-                          
data-flow-interactive-handler="activateReplyPost">'.htmlentities((string)((isset($in['actions']['reply']['title'])
 && is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : 
null), ENT_QUOTES, 'UTF-8').'</a>
+                          data-flow-interactive-handler="activateReplyPost"
+
+                          
+                          data-flow-eventlog-schema="FlowReplies"
+                          data-flow-eventlog-action="initiate"
+                          data-flow-eventlog-entrypoint="reply-post"
+                          data-flow-eventlog-forward="
+                                  < 
.flow-post:not([data-flow-post-max-depth=\'1\']) .flow-reply-form 
[data-role=\'cancel\'],
+                                  < 
.flow-post:not([data-flow-post-max-depth=\'1\']) .flow-reply-form 
[data-role=\'action\'][name=\'preview\'],
+                                  < 
.flow-post:not([data-flow-post-max-depth=\'1\']) .flow-reply-form 
[data-role=\'submit\']
+                          "
+                       
>'.htmlentities((string)((isset($in['actions']['reply']['title']) && 
is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), 
ENT_QUOTES, 'UTF-8').'</a>
                ' : '').'
                '.((LCRun3::ifvar($cx, ((isset($in['actions']['edit']) && 
is_array($in['actions'])) ? $in['actions']['edit'] : null))) ? '
                        <a 
href="'.htmlentities((string)((isset($in['actions']['edit']['url']) && 
is_array($in['actions']['edit'])) ? $in['actions']['edit']['url'] : null), 
ENT_QUOTES, 'UTF-8').'"
@@ -253,12 +264,16 @@
         name="preview"
         data-role="action"
         class="mw-ui-button mw-ui-progressive mw-ui-quiet mw-ui-flush-right 
flow-js"
+        data-flow-eventlog-action="preview"
 >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-preview'),Array()), 
 >'encq').'</button>
 
 <button data-flow-interactive-handler="cancelForm"
         data-role="cancel"
         type="reset"
         class="mw-ui-button mw-ui-destructive mw-ui-quiet mw-ui-flush-right 
flow-js"
+
+        
+        
 >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-cancel'),Array()), 
 >'encq').'</button>
 ';},'flow_edit_post' => function ($cx, $in) {return '<form 
class="flow-edit-post-form"
       method="POST"
@@ -302,17 +317,23 @@
                                required
                                data-flow-preview-template="flow_post"
                                data-flow-expandable="true"
-                               class="mw-ui-input"
+                               class="mw-ui-input flow-click-interactive"
                                type="text"
                                placeholder="'.LCRun3::ch($cx, 'l10n', 
Array(Array('flow-reply-topic-title-placeholder',((isset($in['properties']['topic-of-post'])
 && is_array($in['properties'])) ? $in['properties']['topic-of-post'] : 
null)),Array()), 'encq').'"
-                               data-role="content">'.((LCRun3::ifvar($cx, 
((isset($cx['scopes'][0]['submitted']) && is_array($cx['scopes'][0])) ? 
$cx['scopes'][0]['submitted'] : null))) ? ''.LCRun3::hbch($cx, 'ifCond', 
Array(Array(((isset($cx['scopes'][0]['submitted']['postId']) && 
is_array($cx['scopes'][0]['submitted'])) ? 
$cx['scopes'][0]['submitted']['postId'] : null),'===',((isset($in['postId']) && 
is_array($in)) ? $in['postId'] : null)),Array()), $in, function($cx, $in) 
{return 
''.htmlentities((string)((isset($cx['scopes'][0]['submitted']['content']) && 
is_array($cx['scopes'][0]['submitted'])) ? 
$cx['scopes'][0]['submitted']['content'] : null), ENT_QUOTES, 'UTF-8').'';}).'' 
: '').'</textarea>
+                               data-role="content"
+
+                               
+                               
data-flow-interactive-handler-focus="activateReplyTopic"
+               >'.((LCRun3::ifvar($cx, ((isset($cx['scopes'][0]['submitted']) 
&& is_array($cx['scopes'][0])) ? $cx['scopes'][0]['submitted'] : null))) ? 
''.LCRun3::hbch($cx, 'ifCond', 
Array(Array(((isset($cx['scopes'][0]['submitted']['postId']) && 
is_array($cx['scopes'][0]['submitted'])) ? 
$cx['scopes'][0]['submitted']['postId'] : null),'===',((isset($in['postId']) && 
is_array($in)) ? $in['postId'] : null)),Array()), $in, function($cx, $in) 
{return 
''.htmlentities((string)((isset($cx['scopes'][0]['submitted']['content']) && 
is_array($cx['scopes'][0]['submitted'])) ? 
$cx['scopes'][0]['submitted']['content'] : null), ENT_QUOTES, 'UTF-8').'';}).'' 
: '').'</textarea>
 
                <div class="flow-form-actions flow-form-collapsible">
                        <button data-role="submit"
                                class="mw-ui-button mw-ui-constructive"
                                data-flow-interactive-handler="apiRequest"
                                data-flow-api-handler="submitReply"
-                               data-flow-api-target="< 
.flow-topic">'.htmlentities((string)((isset($in['actions']['reply']['title']) 
&& is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : 
null), ENT_QUOTES, 'UTF-8').'</button>
+                               data-flow-api-target="< .flow-topic"
+                               data-flow-eventlog-action="save-attempt"
+                       
>'.htmlentities((string)((isset($in['actions']['reply']['title']) && 
is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), 
ENT_QUOTES, 'UTF-8').'</button>
                        '.LCRun3::p($cx, 'flow_form_buttons', 
Array(Array($in),Array())).'
                        <small class="flow-terms-of-use 
plainlinks">'.LCRun3::ch($cx, 'l10nParse', 
Array(Array('flow-terms-of-use-reply'),Array()), 'encq').'</small>
                </div>
diff --git a/handlebars/compiled/flow_block_topic_moderate_topic.handlebars.php 
b/handlebars/compiled/flow_block_topic_moderate_topic.handlebars.php
index fa646a7..3a3d05a 100644
--- a/handlebars/compiled/flow_block_topic_moderate_topic.handlebars.php
+++ b/handlebars/compiled/flow_block_topic_moderate_topic.handlebars.php
@@ -84,7 +84,18 @@
                        <a 
href="'.htmlentities((string)((isset($in['actions']['reply']['url']) && 
is_array($in['actions']['reply'])) ? $in['actions']['reply']['url'] : null), 
ENT_QUOTES, 'UTF-8').'"
                           
title="'.htmlentities((string)((isset($in['actions']['reply']['title']) && 
is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), 
ENT_QUOTES, 'UTF-8').'"
                           class="mw-ui-anchor mw-ui-progressive mw-ui-quiet"
-                          
data-flow-interactive-handler="activateReplyPost">'.htmlentities((string)((isset($in['actions']['reply']['title'])
 && is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : 
null), ENT_QUOTES, 'UTF-8').'</a>
+                          data-flow-interactive-handler="activateReplyPost"
+
+                          
+                          data-flow-eventlog-schema="FlowReplies"
+                          data-flow-eventlog-action="initiate"
+                          data-flow-eventlog-entrypoint="reply-post"
+                          data-flow-eventlog-forward="
+                                  < 
.flow-post:not([data-flow-post-max-depth=\'1\']) .flow-reply-form 
[data-role=\'cancel\'],
+                                  < 
.flow-post:not([data-flow-post-max-depth=\'1\']) .flow-reply-form 
[data-role=\'action\'][name=\'preview\'],
+                                  < 
.flow-post:not([data-flow-post-max-depth=\'1\']) .flow-reply-form 
[data-role=\'submit\']
+                          "
+                       
>'.htmlentities((string)((isset($in['actions']['reply']['title']) && 
is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), 
ENT_QUOTES, 'UTF-8').'</a>
                ' : '').'
                '.((LCRun3::ifvar($cx, ((isset($in['actions']['edit']) && 
is_array($in['actions'])) ? $in['actions']['edit'] : null))) ? '
                        <a 
href="'.htmlentities((string)((isset($in['actions']['edit']['url']) && 
is_array($in['actions']['edit'])) ? $in['actions']['edit']['url'] : null), 
ENT_QUOTES, 'UTF-8').'"
@@ -253,12 +264,16 @@
         name="preview"
         data-role="action"
         class="mw-ui-button mw-ui-progressive mw-ui-quiet mw-ui-flush-right 
flow-js"
+        data-flow-eventlog-action="preview"
 >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-preview'),Array()), 
 >'encq').'</button>
 
 <button data-flow-interactive-handler="cancelForm"
         data-role="cancel"
         type="reset"
         class="mw-ui-button mw-ui-destructive mw-ui-quiet mw-ui-flush-right 
flow-js"
+
+        
+        
 >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-cancel'),Array()), 
 >'encq').'</button>
 ';},'flow_edit_post' => function ($cx, $in) {return '<form 
class="flow-edit-post-form"
       method="POST"
@@ -302,17 +317,23 @@
                                required
                                data-flow-preview-template="flow_post"
                                data-flow-expandable="true"
-                               class="mw-ui-input"
+                               class="mw-ui-input flow-click-interactive"
                                type="text"
                                placeholder="'.LCRun3::ch($cx, 'l10n', 
Array(Array('flow-reply-topic-title-placeholder',((isset($in['properties']['topic-of-post'])
 && is_array($in['properties'])) ? $in['properties']['topic-of-post'] : 
null)),Array()), 'encq').'"
-                               data-role="content">'.((LCRun3::ifvar($cx, 
((isset($cx['scopes'][0]['submitted']) && is_array($cx['scopes'][0])) ? 
$cx['scopes'][0]['submitted'] : null))) ? ''.LCRun3::hbch($cx, 'ifCond', 
Array(Array(((isset($cx['scopes'][0]['submitted']['postId']) && 
is_array($cx['scopes'][0]['submitted'])) ? 
$cx['scopes'][0]['submitted']['postId'] : null),'===',((isset($in['postId']) && 
is_array($in)) ? $in['postId'] : null)),Array()), $in, function($cx, $in) 
{return 
''.htmlentities((string)((isset($cx['scopes'][0]['submitted']['content']) && 
is_array($cx['scopes'][0]['submitted'])) ? 
$cx['scopes'][0]['submitted']['content'] : null), ENT_QUOTES, 'UTF-8').'';}).'' 
: '').'</textarea>
+                               data-role="content"
+
+                               
+                               
data-flow-interactive-handler-focus="activateReplyTopic"
+               >'.((LCRun3::ifvar($cx, ((isset($cx['scopes'][0]['submitted']) 
&& is_array($cx['scopes'][0])) ? $cx['scopes'][0]['submitted'] : null))) ? 
''.LCRun3::hbch($cx, 'ifCond', 
Array(Array(((isset($cx['scopes'][0]['submitted']['postId']) && 
is_array($cx['scopes'][0]['submitted'])) ? 
$cx['scopes'][0]['submitted']['postId'] : null),'===',((isset($in['postId']) && 
is_array($in)) ? $in['postId'] : null)),Array()), $in, function($cx, $in) 
{return 
''.htmlentities((string)((isset($cx['scopes'][0]['submitted']['content']) && 
is_array($cx['scopes'][0]['submitted'])) ? 
$cx['scopes'][0]['submitted']['content'] : null), ENT_QUOTES, 'UTF-8').'';}).'' 
: '').'</textarea>
 
                <div class="flow-form-actions flow-form-collapsible">
                        <button data-role="submit"
                                class="mw-ui-button mw-ui-constructive"
                                data-flow-interactive-handler="apiRequest"
                                data-flow-api-handler="submitReply"
-                               data-flow-api-target="< 
.flow-topic">'.htmlentities((string)((isset($in['actions']['reply']['title']) 
&& is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : 
null), ENT_QUOTES, 'UTF-8').'</button>
+                               data-flow-api-target="< .flow-topic"
+                               data-flow-eventlog-action="save-attempt"
+                       
>'.htmlentities((string)((isset($in['actions']['reply']['title']) && 
is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), 
ENT_QUOTES, 'UTF-8').'</button>
                        '.LCRun3::p($cx, 'flow_form_buttons', 
Array(Array($in),Array())).'
                        <small class="flow-terms-of-use 
plainlinks">'.LCRun3::ch($cx, 'l10nParse', 
Array(Array('flow-terms-of-use-reply'),Array()), 'encq').'</small>
                </div>
diff --git a/handlebars/compiled/flow_block_topiclist.handlebars.php 
b/handlebars/compiled/flow_block_topiclist.handlebars.php
index 2fc5637..9e87b2e 100644
--- a/handlebars/compiled/flow_block_topiclist.handlebars.php
+++ b/handlebars/compiled/flow_block_topiclist.handlebars.php
@@ -106,12 +106,17 @@
         name="preview"
         data-role="action"
         class="mw-ui-button mw-ui-progressive mw-ui-quiet mw-ui-flush-right 
flow-js"
+
+        
 >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-preview'),Array()), 
 >'encq').'</button>
 
 <button data-flow-interactive-handler="cancelForm"
         data-role="cancel"
         type="reset"
         class="mw-ui-button mw-ui-destructive mw-ui-quiet mw-ui-flush-right 
flow-js"
+
+        
+        
 >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-cancel'),Array()), 
 >'encq').'</button>
 ';},'flow_newtopic_form' => function ($cx, $in) {return 
''.((LCRun3::ifvar($cx, ((isset($in['actions']['newtopic']) && 
is_array($in['actions'])) ? $in['actions']['newtopic'] : null))) ? '
        <form 
action="'.htmlentities((string)((isset($in['actions']['newtopic']['url']) && 
is_array($in['actions']['newtopic'])) ? $in['actions']['newtopic']['url'] : 
null), ENT_QUOTES, 'UTF-8').'" method="POST" class="flow-newtopic-form" 
data-flow-initial-state="collapsed">
@@ -125,17 +130,27 @@
                <input type="hidden" name="topiclist_replyTo" 
value="'.htmlentities((string)((isset($in['workflowId']) && is_array($in)) ? 
$in['workflowId'] : null), ENT_QUOTES, 'UTF-8').'" />
                <input name="topiclist_topic" class="mw-ui-input 
mw-ui-input-large"
                        required
-                       type="text" placeholder="'.LCRun3::ch($cx, 'l10n', 
Array(Array('flow-newtopic-start-placeholder'),Array()), 'encq').'" 
data-role="title"/>
+                       type="text"
+                       placeholder="'.LCRun3::ch($cx, 'l10n', 
Array(Array('flow-newtopic-start-placeholder'),Array()), 'encq').'"
+                       data-role="title"
+
+                       
+                       data-flow-interactive-handler-focus="activateNewTopic"
+               />
                <textarea name="topiclist_content"
                        data-flow-preview-template="flow_topic"
                        class="mw-ui-input flow-form-collapsible 
mw-ui-input-large"
                        '.((LCRun3::ifvar($cx, ((isset($in['isOnFlowBoard']) && 
is_array($in)) ? $in['isOnFlowBoard'] : null))) ? 'style="display:none;"' : 
'').'
-                       placeholder="'.LCRun3::ch($cx, 'l10n', 
Array(Array('flow-newtopic-content-placeholder',((isset($cx['scopes'][0]['title'])
 && is_array($cx['scopes'][0])) ? $cx['scopes'][0]['title'] : null)),Array()), 
'encq').'" data-role="content" required></textarea>
+                       placeholder="'.LCRun3::ch($cx, 'l10n', 
Array(Array('flow-newtopic-content-placeholder',((isset($cx['scopes'][0]['title'])
 && is_array($cx['scopes'][0])) ? $cx['scopes'][0]['title'] : null)),Array()), 
'encq').'"
+                       data-role="content"
+                       required
+               ></textarea>
 
                <div class="flow-form-actions flow-form-collapsible"
                        '.((LCRun3::ifvar($cx, ((isset($in['isOnFlowBoard']) && 
is_array($in)) ? $in['isOnFlowBoard'] : null))) ? 'style="display:none;"' : 
'').'>
                        <button data-role="submit" 
data-flow-api-handler="newTopic"
                                data-flow-interactive-handler="apiRequest"
+                               data-flow-eventlog-action="save-attempt"
                                class="mw-ui-button mw-ui-constructive 
mw-ui-flush-right">'.LCRun3::ch($cx, 'l10n', 
Array(Array('flow-newtopic-save'),Array()), 'encq').'</button>
                        '.LCRun3::p($cx, 'flow_form_buttons', 
Array(Array($in),Array())).'
                        <small class="flow-terms-of-use 
plainlinks">'.LCRun3::ch($cx, 'l10nParse', 
Array(Array('flow-terms-of-use-new-topic'),Array()), 'encq').'</small>
@@ -161,6 +176,16 @@
                   
title="'.htmlentities((string)((isset($in['actions']['reply']['title']) && 
is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), 
ENT_QUOTES, 'UTF-8').'"
                   class="mw-ui-anchor mw-ui-progressive mw-ui-quiet"
                   data-flow-interactive-handler="activateForm"
+
+                  
+                  data-flow-eventlog-schema="FlowReplies"
+                  data-flow-eventlog-action="initiate"
+                  data-flow-eventlog-entrypoint="reply-top"
+                  data-flow-eventlog-forward="
+                      < .flow-topic .flow-reply-form:last 
[data-role=\'cancel\'],
+                      < .flow-topic .flow-reply-form:last 
[data-role=\'action\'][name=\'preview\'],
+                      < .flow-topic .flow-reply-form:last 
[data-role=\'submit\']
+                  "
                
>'.htmlentities((string)((isset($in['actions']['reply']['title']) && 
is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), 
ENT_QUOTES, 'UTF-8').'</a>
                &bull;
        ' : '').'
@@ -307,17 +332,23 @@
                                required
                                data-flow-preview-template="flow_post"
                                data-flow-expandable="true"
-                               class="mw-ui-input"
+                               class="mw-ui-input flow-click-interactive"
                                type="text"
                                placeholder="'.LCRun3::ch($cx, 'l10n', 
Array(Array('flow-reply-topic-title-placeholder',((isset($in['properties']['topic-of-post'])
 && is_array($in['properties'])) ? $in['properties']['topic-of-post'] : 
null)),Array()), 'encq').'"
-                               data-role="content">'.((LCRun3::ifvar($cx, 
((isset($cx['scopes'][0]['submitted']) && is_array($cx['scopes'][0])) ? 
$cx['scopes'][0]['submitted'] : null))) ? ''.LCRun3::hbch($cx, 'ifCond', 
Array(Array(((isset($cx['scopes'][0]['submitted']['postId']) && 
is_array($cx['scopes'][0]['submitted'])) ? 
$cx['scopes'][0]['submitted']['postId'] : null),'===',((isset($in['postId']) && 
is_array($in)) ? $in['postId'] : null)),Array()), $in, function($cx, $in) 
{return 
''.htmlentities((string)((isset($cx['scopes'][0]['submitted']['content']) && 
is_array($cx['scopes'][0]['submitted'])) ? 
$cx['scopes'][0]['submitted']['content'] : null), ENT_QUOTES, 'UTF-8').'';}).'' 
: '').'</textarea>
+                               data-role="content"
+
+                               
+                               
data-flow-interactive-handler-focus="activateReplyTopic"
+               >'.((LCRun3::ifvar($cx, ((isset($cx['scopes'][0]['submitted']) 
&& is_array($cx['scopes'][0])) ? $cx['scopes'][0]['submitted'] : null))) ? 
''.LCRun3::hbch($cx, 'ifCond', 
Array(Array(((isset($cx['scopes'][0]['submitted']['postId']) && 
is_array($cx['scopes'][0]['submitted'])) ? 
$cx['scopes'][0]['submitted']['postId'] : null),'===',((isset($in['postId']) && 
is_array($in)) ? $in['postId'] : null)),Array()), $in, function($cx, $in) 
{return 
''.htmlentities((string)((isset($cx['scopes'][0]['submitted']['content']) && 
is_array($cx['scopes'][0]['submitted'])) ? 
$cx['scopes'][0]['submitted']['content'] : null), ENT_QUOTES, 'UTF-8').'';}).'' 
: '').'</textarea>
 
                <div class="flow-form-actions flow-form-collapsible">
                        <button data-role="submit"
                                class="mw-ui-button mw-ui-constructive"
                                data-flow-interactive-handler="apiRequest"
                                data-flow-api-handler="submitReply"
-                               data-flow-api-target="< 
.flow-topic">'.htmlentities((string)((isset($in['actions']['reply']['title']) 
&& is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : 
null), ENT_QUOTES, 'UTF-8').'</button>
+                               data-flow-api-target="< .flow-topic"
+                               data-flow-eventlog-action="save-attempt"
+                       
>'.htmlentities((string)((isset($in['actions']['reply']['title']) && 
is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), 
ENT_QUOTES, 'UTF-8').'</button>
                        '.LCRun3::p($cx, 'flow_form_buttons', 
Array(Array($in),Array())).'
                        <small class="flow-terms-of-use 
plainlinks">'.LCRun3::ch($cx, 'l10nParse', 
Array(Array('flow-terms-of-use-reply'),Array()), 'encq').'</small>
                </div>
@@ -412,4 +443,4 @@
 </div>
 ';
 }
-?>
+?>
\ No newline at end of file
diff --git a/handlebars/compiled/flow_block_topiclist_newtopic.handlebars.php 
b/handlebars/compiled/flow_block_topiclist_newtopic.handlebars.php
index 8af2a89..09383e7 100644
--- a/handlebars/compiled/flow_block_topiclist_newtopic.handlebars.php
+++ b/handlebars/compiled/flow_block_topiclist_newtopic.handlebars.php
@@ -50,12 +50,17 @@
         name="preview"
         data-role="action"
         class="mw-ui-button mw-ui-progressive mw-ui-quiet mw-ui-flush-right 
flow-js"
+
+        
 >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-preview'),Array()), 
 >'encq').'</button>
 
 <button data-flow-interactive-handler="cancelForm"
         data-role="cancel"
         type="reset"
         class="mw-ui-button mw-ui-destructive mw-ui-quiet mw-ui-flush-right 
flow-js"
+
+        
+        
 >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-cancel'),Array()), 
 >'encq').'</button>
 ';},'flow_newtopic_form' => function ($cx, $in) {return 
''.((LCRun3::ifvar($cx, ((isset($in['actions']['newtopic']) && 
is_array($in['actions'])) ? $in['actions']['newtopic'] : null))) ? '
        <form 
action="'.htmlentities((string)((isset($in['actions']['newtopic']['url']) && 
is_array($in['actions']['newtopic'])) ? $in['actions']['newtopic']['url'] : 
null), ENT_QUOTES, 'UTF-8').'" method="POST" class="flow-newtopic-form" 
data-flow-initial-state="collapsed">
@@ -69,17 +74,27 @@
                <input type="hidden" name="topiclist_replyTo" 
value="'.htmlentities((string)((isset($in['workflowId']) && is_array($in)) ? 
$in['workflowId'] : null), ENT_QUOTES, 'UTF-8').'" />
                <input name="topiclist_topic" class="mw-ui-input 
mw-ui-input-large"
                        required
-                       type="text" placeholder="'.LCRun3::ch($cx, 'l10n', 
Array(Array('flow-newtopic-start-placeholder'),Array()), 'encq').'" 
data-role="title"/>
+                       type="text"
+                       placeholder="'.LCRun3::ch($cx, 'l10n', 
Array(Array('flow-newtopic-start-placeholder'),Array()), 'encq').'"
+                       data-role="title"
+
+                       
+                       data-flow-interactive-handler-focus="activateNewTopic"
+               />
                <textarea name="topiclist_content"
                        data-flow-preview-template="flow_topic"
                        class="mw-ui-input flow-form-collapsible 
mw-ui-input-large"
                        '.((LCRun3::ifvar($cx, ((isset($in['isOnFlowBoard']) && 
is_array($in)) ? $in['isOnFlowBoard'] : null))) ? 'style="display:none;"' : 
'').'
-                       placeholder="'.LCRun3::ch($cx, 'l10n', 
Array(Array('flow-newtopic-content-placeholder',((isset($cx['scopes'][0]['title'])
 && is_array($cx['scopes'][0])) ? $cx['scopes'][0]['title'] : null)),Array()), 
'encq').'" data-role="content" required></textarea>
+                       placeholder="'.LCRun3::ch($cx, 'l10n', 
Array(Array('flow-newtopic-content-placeholder',((isset($cx['scopes'][0]['title'])
 && is_array($cx['scopes'][0])) ? $cx['scopes'][0]['title'] : null)),Array()), 
'encq').'"
+                       data-role="content"
+                       required
+               ></textarea>
 
                <div class="flow-form-actions flow-form-collapsible"
                        '.((LCRun3::ifvar($cx, ((isset($in['isOnFlowBoard']) && 
is_array($in)) ? $in['isOnFlowBoard'] : null))) ? 'style="display:none;"' : 
'').'>
                        <button data-role="submit" 
data-flow-api-handler="newTopic"
                                data-flow-interactive-handler="apiRequest"
+                               data-flow-eventlog-action="save-attempt"
                                class="mw-ui-button mw-ui-constructive 
mw-ui-flush-right">'.LCRun3::ch($cx, 'l10n', 
Array(Array('flow-newtopic-save'),Array()), 'encq').'</button>
                        '.LCRun3::p($cx, 'flow_form_buttons', 
Array(Array($in),Array())).'
                        <small class="flow-terms-of-use 
plainlinks">'.LCRun3::ch($cx, 'l10nParse', 
Array(Array('flow-terms-of-use-new-topic'),Array()), 'encq').'</small>
diff --git a/handlebars/compiled/flow_block_topicsummary_edit.handlebars.php 
b/handlebars/compiled/flow_block_topicsummary_edit.handlebars.php
index 3470667..efe8256 100644
--- a/handlebars/compiled/flow_block_topicsummary_edit.handlebars.php
+++ b/handlebars/compiled/flow_block_topicsummary_edit.handlebars.php
@@ -32,12 +32,17 @@
         name="preview"
         data-role="action"
         class="mw-ui-button mw-ui-progressive mw-ui-quiet mw-ui-flush-right 
flow-js"
+
+        
 >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-preview'),Array()), 
 >'encq').'</button>
 
 <button data-flow-interactive-handler="cancelForm"
         data-role="cancel"
         type="reset"
         class="mw-ui-button mw-ui-destructive mw-ui-quiet mw-ui-flush-right 
flow-js"
+
+        
+        
 >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-cancel'),Array()), 
 >'encq').'</button>
 ';},),
         'scopes' => Array($in),
diff --git a/handlebars/compiled/flow_form_buttons.handlebars.php 
b/handlebars/compiled/flow_form_buttons.handlebars.php
index 2a28d1f..bf781b7 100644
--- a/handlebars/compiled/flow_form_buttons.handlebars.php
+++ b/handlebars/compiled/flow_form_buttons.handlebars.php
@@ -24,12 +24,17 @@
         name="preview"
         data-role="action"
         class="mw-ui-button mw-ui-progressive mw-ui-quiet mw-ui-flush-right 
flow-js"
+
+        
 >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-preview'),Array()), 
 >'encq').'</button>
 
 <button data-flow-interactive-handler="cancelForm"
         data-role="cancel"
         type="reset"
         class="mw-ui-button mw-ui-destructive mw-ui-quiet mw-ui-flush-right 
flow-js"
+
+        
+        
 >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-cancel'),Array()), 
 >'encq').'</button>
 ';
 }
diff --git a/handlebars/compiled/flow_post.handlebars.php 
b/handlebars/compiled/flow_post.handlebars.php
index ff50df3..9082cff 100644
--- a/handlebars/compiled/flow_post.handlebars.php
+++ b/handlebars/compiled/flow_post.handlebars.php
@@ -62,7 +62,18 @@
                        <a 
href="'.htmlentities((string)((isset($in['actions']['reply']['url']) && 
is_array($in['actions']['reply'])) ? $in['actions']['reply']['url'] : null), 
ENT_QUOTES, 'UTF-8').'"
                           
title="'.htmlentities((string)((isset($in['actions']['reply']['title']) && 
is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), 
ENT_QUOTES, 'UTF-8').'"
                           class="mw-ui-anchor mw-ui-progressive mw-ui-quiet"
-                          
data-flow-interactive-handler="activateReplyPost">'.htmlentities((string)((isset($in['actions']['reply']['title'])
 && is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : 
null), ENT_QUOTES, 'UTF-8').'</a>
+                          data-flow-interactive-handler="activateReplyPost"
+
+                          
+                          data-flow-eventlog-schema="FlowReplies"
+                          data-flow-eventlog-action="initiate"
+                          data-flow-eventlog-entrypoint="reply-post"
+                          data-flow-eventlog-forward="
+                                  < 
.flow-post:not([data-flow-post-max-depth=\'1\']) .flow-reply-form 
[data-role=\'cancel\'],
+                                  < 
.flow-post:not([data-flow-post-max-depth=\'1\']) .flow-reply-form 
[data-role=\'action\'][name=\'preview\'],
+                                  < 
.flow-post:not([data-flow-post-max-depth=\'1\']) .flow-reply-form 
[data-role=\'submit\']
+                          "
+                       
>'.htmlentities((string)((isset($in['actions']['reply']['title']) && 
is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), 
ENT_QUOTES, 'UTF-8').'</a>
                ' : '').'
                '.((LCRun3::ifvar($cx, ((isset($in['actions']['edit']) && 
is_array($in['actions'])) ? $in['actions']['edit'] : null))) ? '
                        <a 
href="'.htmlentities((string)((isset($in['actions']['edit']['url']) && 
is_array($in['actions']['edit'])) ? $in['actions']['edit']['url'] : null), 
ENT_QUOTES, 'UTF-8').'"
@@ -231,12 +242,16 @@
         name="preview"
         data-role="action"
         class="mw-ui-button mw-ui-progressive mw-ui-quiet mw-ui-flush-right 
flow-js"
+        data-flow-eventlog-action="preview"
 >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-preview'),Array()), 
 >'encq').'</button>
 
 <button data-flow-interactive-handler="cancelForm"
         data-role="cancel"
         type="reset"
         class="mw-ui-button mw-ui-destructive mw-ui-quiet mw-ui-flush-right 
flow-js"
+
+        
+        
 >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-cancel'),Array()), 
 >'encq').'</button>
 ';},'flow_edit_post' => function ($cx, $in) {return '<form 
class="flow-edit-post-form"
       method="POST"
@@ -280,17 +295,23 @@
                                required
                                data-flow-preview-template="flow_post"
                                data-flow-expandable="true"
-                               class="mw-ui-input"
+                               class="mw-ui-input flow-click-interactive"
                                type="text"
                                placeholder="'.LCRun3::ch($cx, 'l10n', 
Array(Array('flow-reply-topic-title-placeholder',((isset($in['properties']['topic-of-post'])
 && is_array($in['properties'])) ? $in['properties']['topic-of-post'] : 
null)),Array()), 'encq').'"
-                               data-role="content">'.((LCRun3::ifvar($cx, 
((isset($cx['scopes'][0]['submitted']) && is_array($cx['scopes'][0])) ? 
$cx['scopes'][0]['submitted'] : null))) ? ''.LCRun3::hbch($cx, 'ifCond', 
Array(Array(((isset($cx['scopes'][0]['submitted']['postId']) && 
is_array($cx['scopes'][0]['submitted'])) ? 
$cx['scopes'][0]['submitted']['postId'] : null),'===',((isset($in['postId']) && 
is_array($in)) ? $in['postId'] : null)),Array()), $in, function($cx, $in) 
{return 
''.htmlentities((string)((isset($cx['scopes'][0]['submitted']['content']) && 
is_array($cx['scopes'][0]['submitted'])) ? 
$cx['scopes'][0]['submitted']['content'] : null), ENT_QUOTES, 'UTF-8').'';}).'' 
: '').'</textarea>
+                               data-role="content"
+
+                               
+                               
data-flow-interactive-handler-focus="activateReplyTopic"
+               >'.((LCRun3::ifvar($cx, ((isset($cx['scopes'][0]['submitted']) 
&& is_array($cx['scopes'][0])) ? $cx['scopes'][0]['submitted'] : null))) ? 
''.LCRun3::hbch($cx, 'ifCond', 
Array(Array(((isset($cx['scopes'][0]['submitted']['postId']) && 
is_array($cx['scopes'][0]['submitted'])) ? 
$cx['scopes'][0]['submitted']['postId'] : null),'===',((isset($in['postId']) && 
is_array($in)) ? $in['postId'] : null)),Array()), $in, function($cx, $in) 
{return 
''.htmlentities((string)((isset($cx['scopes'][0]['submitted']['content']) && 
is_array($cx['scopes'][0]['submitted'])) ? 
$cx['scopes'][0]['submitted']['content'] : null), ENT_QUOTES, 'UTF-8').'';}).'' 
: '').'</textarea>
 
                <div class="flow-form-actions flow-form-collapsible">
                        <button data-role="submit"
                                class="mw-ui-button mw-ui-constructive"
                                data-flow-interactive-handler="apiRequest"
                                data-flow-api-handler="submitReply"
-                               data-flow-api-target="< 
.flow-topic">'.htmlentities((string)((isset($in['actions']['reply']['title']) 
&& is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : 
null), ENT_QUOTES, 'UTF-8').'</button>
+                               data-flow-api-target="< .flow-topic"
+                               data-flow-eventlog-action="save-attempt"
+                       
>'.htmlentities((string)((isset($in['actions']['reply']['title']) && 
is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), 
ENT_QUOTES, 'UTF-8').'</button>
                        '.LCRun3::p($cx, 'flow_form_buttons', 
Array(Array($in),Array())).'
                        <small class="flow-terms-of-use 
plainlinks">'.LCRun3::ch($cx, 'l10nParse', 
Array(Array('flow-terms-of-use-reply'),Array()), 'encq').'</small>
                </div>
diff --git a/handlebars/flow_form_buttons.handlebars 
b/handlebars/flow_form_buttons.handlebars
index 52d155c..64bda0d 100644
--- a/handlebars/flow_form_buttons.handlebars
+++ b/handlebars/flow_form_buttons.handlebars
@@ -3,6 +3,8 @@
         name="preview"
         data-role="action"
         class="mw-ui-button mw-ui-progressive mw-ui-quiet mw-ui-flush-right 
flow-js"
+
+        {{!-- No data-flow-eventlog-action here; we'll do that in code because 
the action will toggle between preview & keep-editing --}}
 >
        {{~l10n "flow-preview"~}}
 </button>
@@ -11,6 +13,9 @@
         data-role="cancel"
         type="reset"
         class="mw-ui-button mw-ui-destructive mw-ui-quiet mw-ui-flush-right 
flow-js"
+
+        {{!-- No data-flow-eventlog-action here; we'll do that in code to make 
sure it's run before cancel-success & cancel-abort --}}
+        {{!-- funnel id will have been forwarded to this button though, so we 
can access that from the code --}}
 >
        {{~l10n "flow-cancel"~}}
 </button>
diff --git a/handlebars/flow_newtopic_form.handlebars 
b/handlebars/flow_newtopic_form.handlebars
index a798e70..e43c731 100644
--- a/handlebars/flow_newtopic_form.handlebars
+++ b/handlebars/flow_newtopic_form.handlebars
@@ -10,17 +10,32 @@
                <input type="hidden" name="topiclist_replyTo" value="{{ 
workflowId }}" />
                <input name="topiclist_topic" class="mw-ui-input 
mw-ui-input-large"
                        required
-                       type="text" placeholder="{{l10n 
"flow-newtopic-start-placeholder"}}" data-role="title"/>
+                       type="text"
+                       placeholder="{{l10n "flow-newtopic-start-placeholder"}}"
+                       data-role="title"
+
+                       {{!--
+                               You'd expect data-flow-eventlog-* data here 
(this one
+                               needs to be clicked to expand the form). That 
stuff will be
+                               in JS though, since we only want it on initial 
focus (activating
+                               the form)
+                       --}}
+                       data-flow-interactive-handler-focus="activateNewTopic"
+               />
                <textarea name="topiclist_content"
                        data-flow-preview-template="flow_topic"
                        class="mw-ui-input flow-form-collapsible 
mw-ui-input-large"
                        {{#if isOnFlowBoard}}style="display:none;"{{/if}}
-                       placeholder="{{l10n "flow-newtopic-content-placeholder" 
@root.title}}" data-role="content" required></textarea>
+                       placeholder="{{l10n "flow-newtopic-content-placeholder" 
@root.title}}"
+                       data-role="content"
+                       required
+               ></textarea>
 
                <div class="flow-form-actions flow-form-collapsible"
                        {{#if isOnFlowBoard}}style="display:none;"{{/if}}>
                        <button data-role="submit" 
data-flow-api-handler="newTopic"
                                data-flow-interactive-handler="apiRequest"
+                               data-flow-eventlog-action="save-attempt"
                                class="mw-ui-button mw-ui-constructive 
mw-ui-flush-right">{{l10n "flow-newtopic-save"}}</button>
                        {{> flow_form_buttons }}
                        <small class="flow-terms-of-use plainlinks">{{l10nParse 
"flow-terms-of-use-new-topic"}}</small>
diff --git a/handlebars/flow_post_meta_actions.handlebars 
b/handlebars/flow_post_meta_actions.handlebars
index ca0d74b..1ed10d3 100644
--- a/handlebars/flow_post_meta_actions.handlebars
+++ b/handlebars/flow_post_meta_actions.handlebars
@@ -4,7 +4,21 @@
                        <a href="{{actions.reply.url}}"
                           title="{{actions.reply.title}}"
                           class="mw-ui-anchor mw-ui-progressive mw-ui-quiet"
-                          data-flow-interactive-handler="activateReplyPost">
+                          data-flow-interactive-handler="activateReplyPost"
+
+                          {{!--
+                                  Initialize EventLogging: see 
flow_topic_titlebar_content for
+                                  more details on how this works.
+                          --}}
+                          data-flow-eventlog-schema="FlowReplies"
+                          data-flow-eventlog-action="initiate"
+                          data-flow-eventlog-entrypoint="reply-post"
+                          data-flow-eventlog-forward="
+                                  < 
.flow-post:not([data-flow-post-max-depth='1']) .flow-reply-form 
[data-role='cancel'],
+                                  < 
.flow-post:not([data-flow-post-max-depth='1']) .flow-reply-form 
[data-role='action'][name='preview'],
+                                  < 
.flow-post:not([data-flow-post-max-depth='1']) .flow-reply-form 
[data-role='submit']
+                          "
+                       >
                                {{~actions.reply.title~}}
                        </a>
                {{/if}}
diff --git a/handlebars/flow_reply_form.handlebars 
b/handlebars/flow_reply_form.handlebars
index 7429135..ce58032 100644
--- a/handlebars/flow_reply_form.handlebars
+++ b/handlebars/flow_reply_form.handlebars
@@ -18,10 +18,24 @@
                                required
                                data-flow-preview-template="flow_post"
                                data-flow-expandable="true"
-                               class="mw-ui-input"
+                               class="mw-ui-input flow-click-interactive"
                                type="text"
                                placeholder="{{l10n 
"flow-reply-topic-title-placeholder" properties.topic-of-post}}"
-                               data-role="content">
+                               data-role="content"
+
+                               {{!--
+                                       You'd expect data-flow-eventlog-* data 
here (this one
+                                       needs to be clicked to expand the form).
+                                       However, this form is used in multiple 
places: as topic-
+                                       level reply form (activated by clicking 
the textarea to
+                                       expand), or to reply to a post 
(activated by clicking the
+                                       "reply" link).
+                                       We only want to track the former, so 
we'll do that in JS so
+                                       we can ignore all focuses for this 
textarea when it's not
+                                       used to activate the topic-level reply 
form.
+                               --}}
+                               
data-flow-interactive-handler-focus="activateReplyTopic"
+               >
                        {{~#if @root.submitted~}}
                                {{~#ifCond @root.submitted.postId "===" 
postId~}}
                                        {{[email protected]~}}
@@ -34,7 +48,9 @@
                                class="mw-ui-button mw-ui-constructive"
                                data-flow-interactive-handler="apiRequest"
                                data-flow-api-handler="submitReply"
-                               data-flow-api-target="< .flow-topic">
+                               data-flow-api-target="< .flow-topic"
+                               data-flow-eventlog-action="save-attempt"
+                       >
                                {{~actions.reply.title~}}
                        </button>
                        {{> flow_form_buttons }}
diff --git a/handlebars/flow_topic_titlebar_content.handlebars 
b/handlebars/flow_topic_titlebar_content.handlebars
index 5036df7..f4a3f3d 100644
--- a/handlebars/flow_topic_titlebar_content.handlebars
+++ b/handlebars/flow_topic_titlebar_content.handlebars
@@ -5,6 +5,29 @@
                   title="{{actions.reply.title}}"
                   class="mw-ui-anchor mw-ui-progressive mw-ui-quiet"
                   data-flow-interactive-handler="activateForm"
+
+                  {{!--
+                      Initialize EventLogging:
+                      * action: name of the action param
+                      * schema: name of the schema (will be forwarded)
+                      * entrypoint: name of the entrypoint (will be forwarded)
+                      * forward: nodes to forward this funnel to
+                      We want to keep track of multiple actions in the same 
"funnel".
+                      Having a node without data-flow-eventlog-funnel-id (this 
node)
+                      will result in a funnel being created. That funnel id 
will then
+                      be forwarded to all specified nodes, so if you later 
click on one
+                      of the forwarded nodes, it'll recognize and find the 
funnel. All
+                      that is needed there, is a specific 
data-flow-eventlog-action,
+                      all other details (log, entrypoint, funnel id, ...) are 
inherited
+                  --}}
+                  data-flow-eventlog-schema="FlowReplies"
+                  data-flow-eventlog-action="initiate"
+                  data-flow-eventlog-entrypoint="reply-top"
+                  data-flow-eventlog-forward="
+                      < .flow-topic .flow-reply-form:last [data-role='cancel'],
+                      < .flow-topic .flow-reply-form:last 
[data-role='action'][name='preview'],
+                      < .flow-topic .flow-reply-form:last [data-role='submit']
+                  "
                >
                        {{~actions.reply.title~}}
                </a>
diff --git a/modules/engine/components/board/base/flow-board-api-events.js 
b/modules/engine/components/board/base/flow-board-api-events.js
index ff34899..dc6f305 100644
--- a/modules/engine/components/board/base/flow-board-api-events.js
+++ b/modules/engine/components/board/base/flow-board-api-events.js
@@ -164,7 +164,12 @@
        FlowBoardComponentApiEventsMixin.UI.events.apiPreHandlers.preview = 
function ( event ) {
                var callback,
                        $this = $( this ),
-                       flowBoard = mw.flow.getPrototypeMethod( 'board', 
'getInstanceByElement' )( $this );
+                       flowBoard = mw.flow.getPrototypeMethod( 'board', 
'getInstanceByElement' )( $this ),
+                       schemaName = $( this ).data( 'flow-eventlog-schema' ),
+                       funnelId = $( this ).data( 'flow-eventlog-funnel-id' ),
+                       logAction = $( this ).data( 'flow-return-to-edit' ) ? 
'keep-editing' : 'preview';
+
+               flowBoard.logEvent( schemaName, { action: logAction, funnelId: 
funnelId } );
 
                callback = function ( queryMap ) {
                        var content = null;
@@ -691,13 +696,14 @@
 
                // Hide cancel button on preview screen
                $cancelButton.hide();
+
                // Assign the reset-preview information for later use
                $button
                        .data( 'flow-return-to-edit', {
                                text: $button.text(),
-                               $nodes: $previewContainer
+                               $nodes: $previewContainer,
                        } )
-                       .text( 
flowBoard.constructor.static.TemplateEngine.l10n('flow-preview-return-edit-post')
 )
+                       .text( 
flowBoard.constructor.static.TemplateEngine.l10n( 
'flow-preview-return-edit-post' ) )
                        .one( 'click', function() {
                                $cancelButton.show();
                        } );
@@ -714,12 +720,16 @@
         */
        FlowBoardComponentApiEventsMixin.UI.events.apiHandlers.newTopic = 
function ( info, data, jqxhr ) {
                var result, html,
+                       schemaName = $( this ).data( 'flow-eventlog-schema' ),
+                       funnelId = $( this ).data( 'flow-eventlog-funnel-id' ),
                        flowBoard = mw.flow.getPrototypeMethod( 'board', 
'getInstanceByElement' )( $( this ) );
 
                if ( info.status !== 'done' ) {
                        // Error will be displayed by default, nothing else to 
wrap up
                        return $.Deferred().reject().promise();
                }
+
+               flowBoard.logEvent( schemaName, { action: 'save-success', 
funnelId: funnelId } );
 
                result = data.flow['new-topic'].result.topiclist;
 
@@ -751,13 +761,18 @@
         */
        FlowBoardComponentApiEventsMixin.UI.events.apiHandlers.submitReply = 
function ( info, data, jqxhr ) {
                var $form = $( this ).closest( 'form' ),
-                       flowBoard = mw.flow.getPrototypeMethod( 'board', 
'getInstanceByElement' )( $form );
+                       flowBoard = mw.flow.getPrototypeMethod( 'board', 
'getInstanceByElement' )( $form ),
+                       schemaName = $( this ).data( 'flow-eventlog-schema' ),
+                       funnelId = $( this ).data( 'flow-eventlog-funnel-id' );
 
                if ( info.status !== 'done' ) {
                        // Error will be displayed by default, nothing else to 
wrap up
                        return $.Deferred().reject().promise();
                }
 
+               flowBoard.logEvent( schemaName, { action: 'save-success', 
funnelId: funnelId } );
+
+               // Execute cancel callback to destroy form
                flowBoard.emitWithReturn( 'cancelForm', $form );
 
                // Target should be flow-topic
diff --git 
a/modules/engine/components/board/base/flow-board-interactive-events.js 
b/modules/engine/components/board/base/flow-board-interactive-events.js
index 6663b01..0659808 100644
--- a/modules/engine/components/board/base/flow-board-interactive-events.js
+++ b/modules/engine/components/board/base/flow-board-interactive-events.js
@@ -168,6 +168,82 @@
         * @param {Event} event
         * @returns {$.Promise}
         */
+       
FlowBoardComponentInteractiveEventsMixin.UI.events.interactiveHandlers.activateReplyTopic
 = function ( event ) {
+               var $topic = $( this ).closest( '.flow-topic' ),
+                       topicId = $topic.data( 'flow-id' ),
+                       component;
+
+               // The reply form is used in multiple places. This will check 
if it was
+               // triggered from inside the topic reply form.
+               if ( $( this ).closest( '#flow-reply-' + topicId ).length === 0 
) {
+                       // Not in topic reply form
+                       return $.Deferred().reject();
+               }
+
+               // Only if the textarea is compressed, is it being activated. 
Otherwise,
+               // it has already expanded and this focus is now just 
re-focussing the
+               // already active form
+               if ( !$( this ).hasClass( 'flow-input-compressed' ) ) {
+                       // Form already activated
+                       return $.Deferred().reject();
+               }
+
+               component = mw.flow.getPrototypeMethod( 'component', 
'getInstanceByElement' )( $( this ) );
+               component.logEvent(
+                       'FlowReplies',
+                       // log data
+                       {
+                               entrypoint: 'reply-bottom',
+                               action: 'initiate'
+                       },
+                       // nodes to forward funnel to
+                       $( this ).findWithParent(
+                               '< .flow-reply-form [data-role="cancel"],' +
+                               '< .flow-reply-form 
[data-role="action"][name="preview"],' +
+                               '< .flow-reply-form [data-role="submit"]'
+                       )
+               );
+
+               return $.Deferred().resolve();
+       };
+
+       /**
+        * @param {Event} event
+        */
+       
FlowBoardComponentInteractiveEventsMixin.UI.events.interactiveHandlers.activateNewTopic
 = function ( event ) {
+               var $form = $( this ).closest( '.flow-newtopic-form' ),
+                       component;
+
+               // Only if the textarea is compressed, is it being activated. 
Otherwise,
+               // it has already expanded and this focus is now just 
re-focussing the
+               // already active form
+               if ( $form.find( '.flow-input-compressed' ).length === 0 ) {
+                       // Form already activated
+                       return $.Deferred().reject();
+               }
+
+               component = mw.flow.getPrototypeMethod( 'component', 
'getInstanceByElement' )( $( this ) );
+               component.logEvent(
+                       'FlowReplies',
+                       // log data
+                       {
+                               entrypoint: 'new-topic',
+                               action: 'initiate'
+                       },
+                       // nodes to forward funnel to
+                       $( this ).findWithParent(
+                               '< .flow-newtopic-form [data-role="cancel"],' +
+                               '< .flow-newtopic-form 
[data-role="action"][name="preview"],' +
+                               '< .flow-newtopic-form [data-role="submit"]'
+                       )
+               );
+
+               return $.Deferred().resolve();
+       };
+
+       /**
+        * @param {Event} event
+        */
        
FlowBoardComponentInteractiveEventsMixin.UI.events.interactiveHandlers.activateReplyPost
 = function ( event ) {
                event.preventDefault();
 
diff --git a/modules/engine/components/board/base/flow-board-misc.js 
b/modules/engine/components/board/base/flow-board-misc.js
index 99043c2..dca5813 100644
--- a/modules/engine/components/board/base/flow-board-misc.js
+++ b/modules/engine/components/board/base/flow-board-misc.js
@@ -93,6 +93,80 @@
        }
        FlowBoardComponentMiscMixin.prototype.resetPreview = 
flowBoardComponentResetPreview;
 
+       /**
+        * This will trigger an eventLog call to the given schema with the given
+        * parameters (along with other info about the user & page.)
+        * A unique funnel ID will be created for all new EventLog calls.
+        *
+        * There may be multiple subsequent calls in the same "funnel" (and 
share
+        * same info) that you want to track. It is possible to forward funnel 
data
+        * from one node to another once the first has been clicked. It'll then
+        * log new calls with the same data (schema & entrypoint) & funnel ID 
as the
+        * initial logged event.
+        *
+        * @param {string} schemaName
+        * @param {object} data Data to be logged
+        * @param {string} data.action Schema's action parameter. Always 
required!
+        * @param {string} [data.entrypoint] Schema's entrypoint parameter (can 
be
+        *   omitted if already logged in funnel - will inherit)
+        * @param {string} [data.funnelId] Schema's funnelId parameter (can be
+        *   omitted if starting new funnel - will be generated)
+        * @param {jQuery} [$forward] Nodes to forward funnel to
+        * @returns {object} Logged data
+        */
+       function logEvent( schemaName, data, $forward ) {
+               var // Get existing (forwarded) funnel id, or generate a new 
one if it does not yet exist
+                       funnelId = data.funnelId || 
mw.flow.FlowEventLogRegistry.generateFunnelId(),
+                       // Fetch existing EventLog object for this funnel (if 
any)
+                       eventLog = 
mw.flow.FlowEventLogRegistry.funnels[funnelId];
+
+               // Optional argument, may not want/need to forward funnel to 
other nodes
+               $forward = $forward || $();
+
+               if ( !eventLog ) {
+                       // Add some more data to log!
+                       data = $.extend( data, {
+                               isAnon: mw.user.isAnon(),
+                               sessionId: mw.user.sessionId(),
+                               funnelId: funnelId,
+                               pageNs: mw.config.get( 'wgNamespaceNumber' ),
+                               pageTitle: ( new mw.Title( mw.config.get( 
'wgPageName' ) ) ).getMain()
+                       } );
+
+                       // A funnel with this id does not yet exist, create it!
+                       eventLog = new mw.flow.EventLog( schemaName, data );
+
+                       // Store this particular eventLog - we may want to log 
more things
+                       // in this funnel
+                       mw.flow.FlowEventLogRegistry.funnels[funnelId] = 
eventLog;
+               }
+
+               // Log this action
+               eventLog.logEvent( { action: data.action } );
+
+               // Forward the event
+               this.forwardEvent( $forward, schemaName, funnelId );
+
+               return data;
+       }
+       FlowBoardComponentMiscMixin.prototype.logEvent = logEvent;
+
+       /**
+        * Forward funnel data to other places.
+        *
+        * @param {jQuery} $forward Nodes to forward funnel to
+        * @param {string} schemaName
+        * @param {string} funnelId Schema's funnelId parameter
+        */
+       function forwardEvent( $forward, schemaName, funnelId ) {
+               // Not using data() - it somehow gets lost on some nodes
+               $forward.attr( {
+                       'data-flow-eventlog-schema': schemaName,
+                       'data-flow-eventlog-funnel-id': funnelId
+               } );
+       }
+       FlowBoardComponentMiscMixin.prototype.forwardEvent = forwardEvent;
+
        //
        // Static functions
        //
@@ -232,4 +306,4 @@
 
        // Mixin to FlowBoardComponent
        mw.flow.mixinComponent( 'board', FlowBoardComponentMiscMixin );
-}( jQuery, mediaWiki ) );
\ No newline at end of file
+}( jQuery, mediaWiki ) );
diff --git a/modules/engine/components/board/base/flow-boardandhistory-base.js 
b/modules/engine/components/board/base/flow-boardandhistory-base.js
index 05185ba..f27991c 100644
--- a/modules/engine/components/board/base/flow-boardandhistory-base.js
+++ b/modules/engine/components/board/base/flow-boardandhistory-base.js
@@ -115,9 +115,18 @@
                        flowComponent = mw.flow.getPrototypeMethod( 
'boardAndHistoryBase', 'getInstanceByElement' )( $form ),
                        $fields = $form.find( 'textarea, :text' ),
                        changedFieldCount = 0,
-                       $deferred = $.Deferred();
+                       $deferred = $.Deferred(),
+                       callbacks = $form.data( 'flow-cancel-callback' ) || [],
+                       schemaName = $( this ).data( 'flow-eventlog-schema' ),
+                       funnelId = $( this ).data( 'flow-eventlog-funnel-id' );
 
                event.preventDefault();
+
+               // Only log cancel attempt if it was user-initiated, not when 
the cancel
+               // was triggered by code (as part of a post-submit form destroy)
+               if ( event.which ) {
+                       flowComponent.logEvent( schemaName, { action: 
'cancel-attempt', funnelId: funnelId } );
+               }
 
                // Check for non-empty fields of text
                $fields.each( function () {
@@ -127,31 +136,36 @@
                        }
                } );
 
-               // If all the text fields are empty, OR if the user confirms to 
close this with text already entered, do it.
-               if ( !changedFieldCount || confirm( 
flowComponent.constructor.static.TemplateEngine.l10n( 'flow-cancel-warning' ) ) 
) {
-                       // Reset the form content
-                       $form[0].reset();
+               // Only log if user had already entered text (= confirmation 
was requested)
+               if ( changedFieldCount ) {
+                       if ( confirm( 
flowComponent.constructor.static.TemplateEngine.l10n( 'flow-cancel-warning' ) ) 
) {
+                               flowComponent.logEvent( schemaName, { action: 
'cancel-success', funnelId: funnelId } );
+                       } else {
+                               flowComponent.logEvent( schemaName, { action: 
'cancel-abort', funnelId: funnelId } );
 
-                       // Trigger for flow-actions-disabler
-                       $form.find( 'textarea, :text' ).trigger( 'keyup' );
-
-                       // Hide the form
-                       flowComponent.emitWithReturn( 'hideForm', $form );
-
-                       // Get rid of existing error messages
-                       flowComponent.emitWithReturn( 'removeError', $form );
-
-                       // Trigger the cancel callback
-                       if ( $form.data( 'flow-cancel-callback' ) ) {
-                               $.each( $form.data( 'flow-cancel-callback' ), 
function ( idx, fn ) {
-                                       fn.call( target, event );
-                               } );
+                               // User aborted cancel, quit this function & 
don't destruct the form!
+                               return $deferred.reject().promise();
                        }
-
-                       return $deferred.resolve().promise();
                }
 
-               return $deferred.reject().promise();
+               // Reset the form content
+               $form[0].reset();
+
+               // Trigger for flow-actions-disabler
+               $form.find( 'textarea, :text' ).trigger( 'keyup' );
+
+               // Hide the form
+               flowComponent.emitWithReturn( 'hideForm', $form );
+
+               // Get rid of existing error messages
+               flowComponent.emitWithReturn( 'removeError', $form );
+
+               // Trigger the cancel callback
+               $.each( callbacks, function ( idx, fn ) {
+                       fn.call( target, event );
+               } );
+
+               return $deferred.resolve().promise();
        };
 
        //
diff --git a/modules/engine/components/common/flow-component-engines.js 
b/modules/engine/components/common/flow-component-engines.js
index 2769efd..3d8a928 100644
--- a/modules/engine/components/common/flow-component-engines.js
+++ b/modules/engine/components/common/flow-component-engines.js
@@ -28,6 +28,12 @@
         */
        mw.flow.Api = new mw.flow.FlowApi( 
FlowComponentEnginesMixin.static.StorageEngine );
 
+       /**
+        * EventLogging wrapper
+        * @type {FlowEventLog}
+        */
+       mw.flow.EventLog = mw.flow.FlowEventLog;
+
        // Copy static and prototype from mixin to main class
        mw.flow.mixinComponent( 'component', FlowComponentEnginesMixin );
 }( jQuery, mediaWiki, mediaWiki.flow.vendor.initStorer ) );
diff --git a/modules/engine/components/common/flow-component-events.js 
b/modules/engine/components/common/flow-component-events.js
index 05c730e..f5edc41 100644
--- a/modules/engine/components/common/flow-component-events.js
+++ b/modules/engine/components/common/flow-component-events.js
@@ -59,6 +59,11 @@
                                'focusin.FlowBoardComponent',
                                'input.mw-ui-input, textarea',
                                _getDispatchCallback( this, 'focusField' )
+                       )
+                       .on(
+                               'click.FlowBoardComponent 
keypress.FlowBoardComponent',
+                               '[data-flow-eventlog-action]',
+                               _getDispatchCallback( this, 'eventLogHandler' )
                        );
 
                if ( _isGlobalBound ) {
@@ -319,6 +324,10 @@
                // Remove existing errors from previous attempts
                flowComponent.emitWithReturn( 'removeError', $this );
 
+               // We'll return a deferred object that won't resolve before 
apiHandlers
+               // are resolved
+               $handlerDeferred = $.Deferred();
+
                // If this has a special api handler, bind it to the callback.
                if ( flowComponent.UI.events.apiHandlers[ handlerName ] ) {
                        $deferred
@@ -468,6 +477,8 @@
                        this.debug( 'Failed to find apiHandler', 
apiHandlerName, arguments );
                }
 
+               // Add aggregate deferred object as data attribute, so we can 
hook into
+               // the element when the handlers have run
                $context.data( 'flow-interactive-handler-promise', 
$.when.apply( $, promises ) );
        }
 
@@ -475,7 +486,7 @@
         * Triggers both API and interactive handlers, on click/enter.
         */
        function flowInteractiveHandlerCallback( event ) {
-               // Only trigger with enter key, if keypress
+               // Only trigger with enter key & no modifier keys, if keypress
                if ( event.type === 'keypress' && ( event.charCode !== 13 || 
event.metaKey || event.shiftKey || event.ctrlKey || event.altKey )) {
                        return;
                }
@@ -504,6 +515,68 @@
        FlowComponentEventsMixin.eventHandlers.interactiveHandlerFocus = 
flowInteractiveHandlerFocusCallback;
 
        /**
+        * Callback function for when a [data-flow-eventlog-action] node is 
clicked.
+        * This will trigger a eventLog call to the given schema with the given
+        * parameters.
+        * A unique funnel ID will be created for all new EventLog calls.
+        *
+        * There may be multiple subsequent calls in the same "funnel" (and 
share
+        * same info) that you want to track. It is possible to forward funnel 
data
+        * from one attribute to another once the first has been clicked. It'll 
then
+        * log new calls with the same data (schema & entrypoint) & funnel ID 
as the
+        * initial logged event.
+        *
+        * Required parameters (as data-attributes) are:
+        * * data-flow-eventlog-schema: The schema name
+        * * data-flow-eventlog-entrypoint: The schema's entrypoint parameter
+        * * data-flow-eventlog-action: The schema's action parameter
+        *
+        * Additionally:
+        * * data-flow-eventlog-forward: Selectors to forward funnel data to
+        */
+       function flowEventLogCallback( event ) {
+               // Only trigger with enter key & no modifier keys, if keypress
+               if ( event.type === 'keypress' && ( event.charCode !== 13 || 
event.metaKey || event.shiftKey || event.ctrlKey || event.altKey )) {
+                       return;
+               }
+
+               var $context = $( event.currentTarget ),
+                       data = $context.data(),
+                       component = mw.flow.getPrototypeMethod( 'component', 
'getInstanceByElement' )( $context ),
+                       $promise = data.flowInteractiveHandlerPromise || 
$.Deferred().resolve().promise(),
+                       eventInstance = {},
+                       key, value;
+
+               // Fetch loggable data: everything prefixed flowEventlog
+               for ( key in data ) {
+                       if ( key.indexOf( 'flowEventlog' ) === 0 ) {
+                               // Strips "flowEventlog" and lowercases first 
char after that
+                               value = data[key];
+                               key = key.substr( 12, 1 ).toLowerCase() + 
key.substr( 13 );
+
+                               eventInstance[key] = value;
+                       }
+               }
+
+               // Forward is not loggable data!
+               delete eventInstance.forward;
+
+               // Log the event
+               eventInstance = component.logEvent( data.flowEventlogSchema, 
eventInstance );
+
+               // Promise resolves once all interactiveHandlers/apiHandlers 
are done,
+               // so all nodes we want to forward to are bound to be there
+               $promise.always( function() {
+                       // Now find all nodes to forward to
+                       var $forward = data.flowEventlogForward ? 
$context.findWithParent( data.flowEventlogForward ) : $();
+
+                       // Forward the funnel
+                       eventInstance = component.forwardEvent( $forward, 
data.flowEventlogSchema, eventInstance.funnelId );
+               } );
+       }
+       FlowComponentEventsMixin.eventHandlers.eventLogHandler = 
flowEventLogCallback;
+
+       /**
         * When the whole class has been instantiated fully (after every 
constructor has been called).
         * @param {FlowComponent} component
         */
diff --git a/modules/engine/misc/flow-eventlog.js 
b/modules/engine/misc/flow-eventlog.js
new file mode 100644
index 0000000..aafa751
--- /dev/null
+++ b/modules/engine/misc/flow-eventlog.js
@@ -0,0 +1,45 @@
+( function ( mw, $ ) {
+       /**
+        * @param {String} schemaName Canonical schema name.
+        * @param {Object} [eventInstance] Shared event instance data.
+        * @returns {FlowEventLog}
+        * @constructor
+        */
+       function FlowEventLog( schemaName, eventInstance ) {
+               this.schemaName = schemaName;
+               this.eventInstance = eventInstance || {};
+
+               /**
+                * @param {object} eventInstance Additional event instance data 
for this
+                *   particular event.
+                * @returns {$.Deferred}
+                */
+               function logEvent( eventInstance ) {
+                       // Ensure eventLog & this schema exist, or return a 
stub deferred
+                       if ( !mw.eventLog || 
!mw.eventLog.schemas[this.schemaName] ) {
+                               return $.Deferred().promise();
+                       }
+
+                       return mw.eventLog.logEvent(
+                               this.schemaName,
+                               $.extend( this.eventInstance, eventInstance )
+                       );
+               }
+               this.logEvent = logEvent;
+       }
+
+       var FlowEventLogRegistry = {
+               funnels: {},
+
+               /**
+                * Generates a unique id.
+                *
+                * @returns {string}
+                */
+               generateFunnelId: mw.user.generateRandomSessionId
+       };
+
+       // Export
+       mw.flow.FlowEventLog = FlowEventLog;
+       mw.flow.FlowEventLogRegistry = FlowEventLogRegistry;
+}( mw, jQuery ) );

-- 
To view, visit https://gerrit.wikimedia.org/r/171791
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: Ic011a6130184aadae539eced307bf982f84c990d
Gerrit-PatchSet: 20
Gerrit-Project: mediawiki/extensions/Flow
Gerrit-Branch: master
Gerrit-Owner: Matthias Mullie <[email protected]>
Gerrit-Reviewer: EBernhardson <[email protected]>
Gerrit-Reviewer: Mattflaschen <[email protected]>
Gerrit-Reviewer: Matthias Mullie <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to