diff options
-rw-r--r-- | js/piwik.js | 163 | ||||
-rw-r--r-- | misc/internal-docs/content-tracking.md | 599 | ||||
-rw-r--r-- | plugins/Contents/API.php | 6 | ||||
-rw-r--r-- | plugins/Contents/Actions/ActionContent.php | 2 | ||||
-rw-r--r-- | plugins/Contents/Archiver.php | 7 |
5 files changed, 539 insertions, 238 deletions
diff --git a/js/piwik.js b/js/piwik.js index 624b52327f..d7207c6fd9 100644 --- a/js/piwik.js +++ b/js/piwik.js @@ -1329,8 +1329,8 @@ if (typeof Piwik !== 'object') { CONTENT_NAME_ATTR: 'data-content-name', CONTENT_PIECE_ATTR: 'data-content-piece', CONTENT_PIECE_CLASS: 'piwikContentPiece', - CONTENT_INTERACTION_ATTR: 'data-content-interaction', - CONTENT_INTERACTION_CLASS: 'piwikContentInteraction', + CONTENT_TARGET_ATTR: 'data-content-target', + CONTENT_TARGET_CLASS: 'piwikContentTarget', findContentNodes: function () { @@ -1340,6 +1340,25 @@ if (typeof Piwik !== 'object') { return contentNodes; }, + findContentNodesWithinNode: function (node) { + if (!node) { + return; + } + + var nodes1 = this.findNodesHavingAttribute(node, this.CONTENT_ATTR); + var nodes2 = this.findNodesHavingCssClass(node, this.CONTENT_CLASS); + + if (nodes2 && nodes2.length) { + var index; + for (index = 0; index < nodes2.length; index++) { + nodes1.push(nodes2[index]); + } + } + + nodes1 = this.makeNodesUnique(nodes1); + + return nodes1; + }, findParentContentNode: function (anyNode) { if (!anyNode) { return; @@ -1360,16 +1379,12 @@ if (typeof Piwik !== 'object') { findPieceNode: function (node) { var contentPiece; - contentPiece = query.findFirstNodeHavingAttribute(this.CONTENT_NAME_ATTR); + contentPiece = query.findFirstNodeHavingAttribute(this.CONTENT_PIECE_ATTR); if (!contentPiece) { contentPiece = query.findFirstNodeHavingClass(this.CONTENT_PIECE_CLASS); } - if (!contentPiece) { - contentPiece = query.findFirstNodeHavingClass(this.CONTENT_INTERACTION_CLASS); - } - if (contentPiece) { return contentPiece; } @@ -1378,17 +1393,17 @@ if (typeof Piwik !== 'object') { }, findTargetNodeNoDefault: function (node) { - var target = query.findFirstNodeHavingAttributeWithValue(node, this.CONTENT_INTERACTION_ATTR); + var target = query.findFirstNodeHavingAttributeWithValue(node, this.CONTENT_TARGET_ATTR); if (target) { return target; } - target = query.findFirstNodeHavingAttribute(node, this.CONTENT_INTERACTION_ATTR); + target = query.findFirstNodeHavingAttribute(node, this.CONTENT_TARGET_ATTR); if (target) { return target; } - target = query.findFirstNodeHavingClass(node, this.CONTENT_INTERACTION_CLASS); + target = query.findFirstNodeHavingClass(node, this.CONTENT_TARGET_CLASS); if (target) { return target; } @@ -1436,7 +1451,7 @@ if (typeof Piwik !== 'object') { var nameNode = query.findFirstNodeHavingAttributeWithValue(node, this.CONTENT_PIECE_ATTR); if (nameNode) { - return query.getAttributeValueFromNode(nameNode, this.CONTENT_NAME_ATTR); + return query.getAttributeValueFromNode(nameNode, this.CONTENT_PIECE_ATTR); } var contentNode = this.findPieceNode(node); @@ -1463,8 +1478,8 @@ if (typeof Piwik !== 'object') { { var targetNode = this.findTargetNode(node); - if (query.hasNodeAttributeWithValue(targetNode, this.CONTENT_INTERACTION_ATTR)) { - return query.getAttributeValueFromNode(targetNode, this.CONTENT_INTERACTION_ATTR); + if (query.hasNodeAttributeWithValue(targetNode, this.CONTENT_TARGET_ATTR)) { + return query.getAttributeValueFromNode(targetNode, this.CONTENT_TARGET_ATTR); } if (query.hasNodeAttributeWithValue(targetNode, 'href')) { @@ -1607,8 +1622,8 @@ if (typeof Piwik !== 'object') { link = query.getAttributeValueFromNode(clickNode, 'href'); } - if (!link && query.hasNodeAttributeWithValue(clickNode, this.CONTENT_INTERACTION_ATTR)) { - link = query.getAttributeValueFromNode(clickNode, this.CONTENT_INTERACTION_ATTR); + if (!link && query.hasNodeAttributeWithValue(clickNode, this.CONTENT_TARGET_ATTR)) { + link = query.getAttributeValueFromNode(clickNode, this.CONTENT_TARGET_ATTR); } if (!link) { @@ -1878,6 +1893,7 @@ if (typeof Piwik !== 'object') { // Keeps track of previously tracked content impressions trackedContentImpressions = [], + enableTrackOnlyVisibleContent = false, // Guard against installing the link tracker more than once per Tracker instance linkTrackingInstalled = false, @@ -2770,9 +2786,7 @@ if (typeof Piwik !== 'object') { /* * Log currently visible content pieces */ - function logCurrentlyVisibleContentImpressionsIfNotTrackedYet() { - - var contentNodes = content.findContentNodes(); + function logCurrentlyVisibleContentImpressionsIfNotTrackedYet(contentNodes) { if (!contentNodes || !contentNodes.length) { return; @@ -2790,27 +2804,15 @@ if (typeof Piwik !== 'object') { return; } - var contents = content.collectContent(contentNodes); - - for (index = 0; index < contents; index++) { - // TODO !!! it could happen that a content was tracked while this check is performed if they run both at the same time, maybe we need some kind of lock? - if (wasContentImpressionAlreadyTracked(contents[index])) { - contents.splice(index, 1); - } else { - trackedContentImpressions.push(contents[index]); - } - } - - logContentImpressions(contents, contentNodes); + logAllContentImpressions(contentNodes); } /* * Log all content pieces */ - function logAllContentImpressions() { + function logAllContentImpressions(contentNodes) { - var contentNodes = content.findContentNodes(); - var contents = content.collectContent(contentNodes); + var contents = content.collectContent(contentNodes); logContentImpressions(contents, contentNodes); } @@ -2819,11 +2821,27 @@ if (typeof Piwik !== 'object') { * Log all content pieces */ function logContentImpressions(contents, contentNodes) { + + if (!contents || !contents.length) { + return; + } + + for (index = 0; index < contents; index++) { + // TODO !!! it could happen that a content was tracked while this check is performed if they run + // both at the same time, maybe we need some kind of lock? + if (wasContentImpressionAlreadyTracked(contents[index])) { + contents.splice(index, 1); + } else { + trackedContentImpressions.push(contents[index]); + } + } + if (!contents || !contents.length) { return; } for (index = 0; index < contentNodes.length; index++) { + // TODO we should actually replace the link on the target node, not on the content node !!!IMPORTANT var internalLink = content.findInternalTargetLink(contentNodes[index]); if (internalLink) { content.replaceTargetLink(contentNodes[index], buildTrackingRedirectUrl(internalLink)); @@ -2872,20 +2890,13 @@ if (typeof Piwik !== 'object') { sendRequest(request, configTrackerPause); } - function logContentImpressionNode(node, contentTarget) + function logContentImpressionsWithinNode(node) { - var contentNode = content.findParentContentNode(node); - var content = content.buildContentPiece(contentNode); - - if (!content) { - return; - } - - if (!contentTarget && content.target) { - contentTarget = content.target; - } + // TODO what if user wants to detect only visible... we need to provide disableOnlyVisible + var contentNodes = content.findContentNodesWithinNode(node); + var contents = content.collectContent(contentNodes); - logContentImpression(content.name, content.piece, contentTarget); + logContentImpressions(contents, contentNodes); } function logContentInteraction(contentName, contentPiece, contentInteraction) @@ -4012,6 +4023,8 @@ if (typeof Piwik !== 'object') { * @param mixed customData */ trackPageView: function (customTitle, customData) { + trackedContentImpressions = []; + if (isOverlaySession(configTrackerSiteId)) { trackCallback(function () { injectOverlayScripts(configTrackerUrl, configApiUrl, configTrackerSiteId); @@ -4023,24 +4036,43 @@ if (typeof Piwik !== 'object') { } }, - trackAllContentImpressions: function () { + trackContentImpressions: function () { trackCallback(function () { - trackCallbackOnReady(function () { - logAllContentImpressions(); - }); + if (enableTrackOnlyVisibleContent) { + trackCallbackOnLoad(function () { + // we have to wait till CSS parsed and applied + var contentNodes = content.findContentNodes(); + + logCurrentlyVisibleContentImpressionsIfNotTrackedYet(contentNodes); + }); + } else { + trackCallbackOnReady(function () { + // we have to wait till DOM ready + var contentNodes = content.findContentNodes(); + + logAllContentImpressions(contentNodes); + }); + } }); }, - enableDetectContentImpressionsAutomatically: function (checkOnSroll, timeIntervalInMs) { + enableTrackOnlyVisibleContent: function (checkOnSroll, timeIntervalInMs) { var self = this; var didScroll = false; + if (enableTrackOnlyVisibleContent) { + // already enabled, do not register intervals again + return true; + } + + enableTrackOnlyVisibleContent = true; + trackCallbackOnLoad(function () { function checkContent(intervalInMs) { setTimeout(function () { didScroll = false; - self.trackCurrentlyVisibleContentImpressionsIfNotTrackedYet(); + self.trackContentImpressions(); checkBanners(intervalInMs); }, intervalInMs); } @@ -4049,7 +4081,7 @@ if (typeof Piwik !== 'object') { setTimeout(function () { if (didScroll) { didScroll = false; - self.trackCurrentlyVisibleContentImpressionsIfNotTrackedYet(); + self.trackContentImpressions(); } checkContentIfDidScroll(intervalInMs); @@ -4079,21 +4111,12 @@ if (typeof Piwik !== 'object') { if (timeIntervalInMs && timeIntervalInMs > 0) { timeIntervalInMs = parseInt(timeIntervalInMs, 10); - // TODO provide possibility to cancel interval? or if called again cancel previous one? checkBanners(timeIntervalInMs); } }); }, - trackCurrentlyVisibleContentImpressionsIfNotTrackedYet: function () { - trackCallback(function () { - trackCallbackOnLoad(function () { - logCurrentlyVisibleContentImpressionsIfNotTrackedYet(); - }); - }); - }, - // it must be a node that is set to .piwikTrackContent or [data-track-content] or one of its parents nodes trackContentImpression: function (contentName, contentPiece, contentTarget) { trackCallback(function () { @@ -4102,9 +4125,23 @@ if (typeof Piwik !== 'object') { }, // it must be a node that is set to .piwikTrackContent or [data-track-content] or one of its parents nodes // we might remove this method again - trackContentImpressionNode: function (domNode, contentTarget) { + trackContentImpressionsWithinNode: function (domNode) { trackCallback(function () { - logContentImpressionNode(domNode, contentTarget); + if (enableTrackOnlyVisibleContent) { + trackCallbackOnLoad(function () { + // we have to wait till CSS parsed and applied + var contentNodes = content.findContentNodesWithinNode(domNode); + + logCurrentlyVisibleContentImpressionsIfNotTrackedYet(contentNodes); + }); + } else { + trackCallbackOnReady(function () { + // we have to wait till DOM ready + var contentNodes = content.findContentNodesWithinNode(domNode); + + logAllContentImpressions(contentNodes); + }); + } }); }, diff --git a/misc/internal-docs/content-tracking.md b/misc/internal-docs/content-tracking.md index 25ac0ba31f..ebbc8ebea5 100644 --- a/misc/internal-docs/content-tracking.md +++ b/misc/internal-docs/content-tracking.md @@ -1,14 +1,426 @@ # Technical concept for implementing Content Tracking [#4996](#4996) +See https://github.com/piwik/piwik/issues/4996 for explanation of the actual feature. + This is the technical concept for implementing content tracking. We won't plan anything to death but a little bit of thinking upfront makes sense :) Feel free to contribute and let us know if you have any objections! If your thoughts are not technical please comment on the actual issue [#4996](#4996). ## Naming -* Plugin name: Contents -* Content name - The name of the content visible in reports -* Content piece - eg a video file, image file, text, ... -* Content target - a clicked url, a started video, any "conversion" to be a bit more generic? +| Name | Purpose | +| ------------- | ------------- | +| Plugin name | Contents | +| Content block | Is a container which consists of a content name, piece, target and an interaction. | +| Content name | A name that represents a content block. The name will be visible in reports. One name can belong to differnt content pieces. | +| Content piece | This is the actual content that was displayed, eg a path to a video/image/audio file, a text, ... | +| Content target | For instance the URL of a landing page where the user was led to after interacting with the content block. | +| Content impression | Any content block that was displayed on a page, such as a banner or an ad. Optionally you can tell Piwik to track only impressions for content blocks that were actually visible. | +| Content interaction | Any content block that was interacted with by a user. This means usually a 'click' on a banner or ad happened, but it can be any interaction. | +| Content interaction rate | The ratio of content impressions to interactions. For instance an ad was displayed a 100 times and there were 2 interactions results in a rate of 2%. | + +## Tracking the content declarative + +Generally said you can usually choose between HTML attributes and CSS classes to define the content you want to track. Attributes always take precedence over CSS classes. So if you define an attribute on one element and a CSS class on another element we will always pick the element having the attribute. If you set the same attribute or the same class on multiple elements within one block, the first element will always win. +Nested content blocks are currently not supported. + +HTML attributes are the recommended way to go as it allows you to set a specific value that will be used when detecting the content impressions on your website. +Imagine you do not have a value for an HTML attribute provided or if a CSS class is used, we will have to try to detect the content name, piece and target automatically based on a set of rules which are explained further below. For instance we are trying to read the content target from a `href` attribute of a link, the content piece from a `src` attribute of an image, and the name from a `title` attribute. +If you let us automatically detect those values it can influence your tracking over time. For instance if you provide the same page in different languages, and we will detect the content automatically, we might end up in many different content blocks that represent actually all the same. Therefore it is recommended to use the HTML-attributes including values. + +The following attributes and their corresponding CSS classes are used which will be explained in detail below: +* `[data-track-content] or .piwikTrackContent` == Defines a content block +* `[data-content-name=""]` == Defines the name of the content block +* `[data-content-piece=""] or .piwikContentPiece` == Defines the content piece +* `[data-content-target=""] or .piwikContentTarget` == Defines the content target +* `[data-content-ignoreinteraction] or .piwikContentIgnoreInteraction` == Tells Piwik to not automatically track the interaction + +### How to define a block of content? +You can use either the attribute `data-track-content` or the CSS class `piwikTrackContent`. The attribute does not require any value. + +Examples: +``` +<img src="img-en.jpg" data-track-content/> +// content name = img-en.jpg +// content piece = img-en.jpg +// content target = "" + +<img src="img-en.jpg" class="piwikTrackContent"/> +// content name = img-en.jpg +// content piece = img-en.jpg +// content target = "" +``` + +As you can see in these examples we do detect the content piece and name automatically based on the `src` attribute of the image. The content target cannot be detected since an image does not define a link. + +Note: In the future we may allow to define the name of the content using this attribute instead of `data-content-name` but I did not want this for two reasons: It could also define the actual content (the content piece) so it would not be intuitive, using `data-content-name` attribute allows to set the name also on nested attributes. + +### How do we detect the content piece element? +The content piece element is used to detect the actual content of a content block. + +To find the content piece element we will try to find an element having the attribute `data-content-piece` or the CSS class `piwikContentPiece`. This attribute/class can be specified anywhere within a content block. +If we do not find any specific content piece element, we will use the content block element. + +### How do we detect the content piece? + +* The simplest scenario is to provide an HTML attribute `data-content-piece="foo"` including a value anywhere within the content block or in the content block element itself. +* If there is no such attribute we will check whether the content piece element is a media (audio, video, image) and we will try to detect the URL to the media automatically. For instance using the `src` attribute. +* If we haven't found anything we will fall back to use the value "Unknown". In such a case you should set the attribute `data-content-piece` telling us explicitly what the content is. + +Examples: +``` +<a href="http://www.example.com" data-track-content><img src="img-en.jpg" data-content-piece="img.jpg"/> +// content name = img.jpg +// content piece = img.jpg +// content target = http://www.example.com +``` +As you can see we can now define a specific value for the content piece which can be useful if your text or images are different in for each language. +This time we can also automatically detect the content target since we have set the content block on an `a` element. More about this later. The `data-content-piece` attribute can be set on any element, also in the `a` element. + +``` +<a href="http://www.example.com" data-track-content><img src="img-en.jpg" data-content-piece/> +<a href="http://www.example.com" data-track-content><img src="img-en.jpg" class="piwikContentPiece"/> +// content name = img-en.jpg +// content piece = img-en.jpg +// content target = http://www.example.com +``` + +In this example we were able to detect the name and the piece of the content automatically based on the `src` attribute. + +``` +<a href="http://www.example.com" data-track-content><p data-content-piece>Lorem ipsum dolor sit amet</p> +<a href="http://www.example.com" data-track-content><p class="piwikContentPiece">Lorem ipsum dolor sit amet</p> +// content name = Unknown +// content piece = Unknown +// content target = http://www.example.com +``` + +As the content piece element is not an image, video or audio we cannot detect the content automatically. In such a case you have to define the `data-content-piece` attribute and set a value to it. We do not use the text of this element by default since the text might change often resulting in many content pieces, since it can be very long, since it can be translated and therefore results in many different content pieces although it is always the same, since it might contain user specific content and so on. + +Better: +``` +<a href="http://www.example.com" data-track-content><p data-content-piece="My content">Lorem ipsum dolor sit amet...</p> +// content name = My content +// content piece = My content +// content target = http://www.example.com +``` + +### How do we detect the content name? +The content name represents a content block which will help you in the Piwik UI to easily identify a specific block. + +* The simplest scenario is that you provide us an HTML attribute `data-content-name` with a value anywhere within a content block or in a content block element itself. +* If there is no such element we will use the value of the content piece in case there is one (if !== Unknown). + * A content piece will be usually detected automatically in case the content piece is an image, video or audio element. + * If content piece is a URL that is identical to the current domain of the website we will remove the domain from the URL +* If we do not find a name we will look for a `title` attribute in the content block element. +* If we do not find a name we will look for a `title` attribute in the content piece element. +* If we do not find a name we will look for a `title` attribute in the content target element. +* If we do not find a name we will fall back to "Unknown" + +Examples: +``` +<img src="img-en.jpg" data-track-content data-content-name="Image1"/> +// content name = Image1 +// content piece = img-en.jpg +// content target = "" +``` + +This example would be the way to go by defining a `data-content-name` attribute anywhere we can easily detect the name of the content. + +``` +<img src="img-en.jpg" data-track-content/> +// content name = img-en.jpg +// content piece = img-en.jpg +// content target = "" +``` + +If no content name is set, it will default to the content piece in case there is one. + +``` +<img src="http://www.example.com/path/img-en.jpg" data-track-content/> +// content name = /path/img-en.jpg +// content piece = http://www.example.com/path/img-en.jpg +// content target = "" +``` + +If content piece contains a domain that is the same as the current website's domain we will remove it + +``` +<a href="http://www.example.com" data-track-content>Lorem ipsum dolor sit amet...</p> +// content name = Unknown +// content piece = Unknown +// content target = http://www.example.com +``` + +In case there is no content name, no content piece and no title set anywhere it will default to "Unknown". To get a useful content name you should set either the `data-content-name` or a `title` attribute. + +``` +<a href="http://www.example.com" data-track-content title="Block Title"><span title="Inner Title" data-content-piece>Lorem ipsum dolor sit amet...</p> +// content name = Block Title +// content piece = Unknown +// content target = http://www.example.com +``` + +In case there is no content name and no content piece we will fall back to the `title` attribute of the content block. The `title` attribute of the block element takes precendence over the piece element in this example. + +### How do we detect the content target element? +The content target is the element that we will use to detect the URL of the landing page of the content block. The target element is usually a link or a button element. Generally said the target doesn't have to be a URL it can be anything but in most cases it will be a URL. A target could be for instance also a tab-container + +We detect the target element either by the attribute `data-content-target` or by the class `.piwikContentTarget`. If no such element can be found we will fall back to the content block element. + +### How do we detect the content target URL? + +* The simplest scenario is that you provide us an HTML attribute `data-content-target` with a value anywhere within a content block or in a content block element itself. +* If there is no such element we will look for an `href` attribute in the target element +* If there is no such attribute we will use an empty string "" + +Examples: +``` +<a href="http://www.example.com" data-track-content>Click me</a> +// content name = Unknown +// content piece = Unknown +// content target = "http://www.example.com" +``` + +As no specific target element is set, we will read the `href` attribute of the content block. + +``` +<a onclick="location.href='http://www.example.com'" data-content-target="http://www.example.com" data-track-content>Click me</a> +// content name = Unknown +// content piece = Unknown +// content target = "http://www.example.com" +``` + +No `href` attribute is used as the link is executed via javascript. Therefore a `data-content-target` attribute with value has to be specified. + + +``` +<div data-track-content><input type="submit"/></div> + +// content name = Unknown +// content piece = Unknown +// content target = "" +``` + +As there is neither a `data-content-target` attribute nor a `href` attribute we cannot detect the target. + +``` +<div data-track-content><input type="submit" data-content-target="http://www.example.com"/></div> + +// content name = Unknown +// content piece = Unknown +// content target = "http://www.example.com" +``` + +As the `data-content-target` attribute is specifically set with a value, we can detect the target URL based on this. Otherwise we could not. + +``` +<div data-track-content><a href="http://www.example.com" data-content-target>Click me</a></div> +<div data-track-content><a href="http://www.example.com" class="piwikContentTarget">Click me</a></div> +// content name = Unknown +// content piece = Unknown +// content target = "http://www.example.com" +``` + +As the target element has a `href` attribute we can detect the content target automatically. + +### How do we track an interaction automatically? + +Interactions can be detected declarative in case the detected target element is an `a` element with an `href` attribute. If not, you will have to track +the interaction programmatically, see one of the next sections. We generally treat links to the same page differently than downloads or outlinks. + +#### Links to the same website +In case we detect a link to the same website we will replace the current `href` attribute with a link to the `piwik.php` tracker URL. Whenever a user clicks on such a link we will first send the user to the `piwik.php` of your Piwik installation and then redirect the user from there to the actual page. This click will be tracked as an event. + +If the URL of the replaced `href` attribute changes meanwhile by your code we will respect the new `href` attribute and make sure to update the link with a `piwik.php` URL. Therefore we will add a `click` listener to the element. + +If you have added an `href` attribute after we scanned the DOM for content blocks we can not detect this and an interaction won't be tracked. + +#### Outlinks and downloads +Outlinks and downloads are handled as before. If a user clicks on a download or outlink we will track this action using an XHR. Along with the information of this action we will send the information related to the content block. We will not track an additional event for this. + +#### Anchor links +They are not tracked currently. Will be tracked as an event in V2? + +TODO we need to implement tracking of non `a` links, of anchor links and `a` links not having `href` attribute using XHR and events + +### How to prevent the automatic tracking of an interaction? + +Maybe you do not want us to track any interaction automatically as explained before. +To do so you can either set the attribute `data-content-ignoreinteraction` or the CSS class `piwikContentIgnoreInteraction` on the content target element. + +Examples +``` +<a href="http://outlink.example.com" class="piwikTrackContent piwikContentIgnoreInteraction">Add to shopping cart</a> +<a href="http://outlink.example.com" data-track-content data-content-ignoreinteraction>Add to shopping cart</a> +<div data-track-content><a href="http://outlink.example.com" data-content-target data-content-ignoreinteraction>Add to shopping cart</a></div> +``` + +In all examples we would track the impression automatically but not the interaction. + +### Putting it all together + +A few Examples: + +``` +<div data-track-content data-content-name="My Ad"> + <img src="http://www.example.com/path/xyz.jpg" data-content-piece /> + <a href="/anylink" data-content-target>Add to shopping cart</a> +</div> +// content name = My Ad +// content piece = http://www.example.com/path/xyz.jpg +// content target = /anylink +``` + +A typical example for a content block that displays an image - which is the content piece - and a call to action link - which is the content target - below. +We would replace the `href=/anylink` with a link to piwik.php of your Piwik installation which will in turn redirect the user to the actual target to actually track the interaction. + +``` +<a href="http://ad.example.com" data-track-content> + <img src="http://www.example.com/path/xyz.jpg" data-content-piece /> +</a> +// content name = xyz.jpg +// content piece = http://www.example.com/path/xyz.jpg +// content target = http://ad.example.com +``` + +A typical example for a content block that displays a banner ad. + +``` +<a href="http://ad.example.com" data-track-content data-content-name="My Ad"> + Lorem ipsum.... +</a> +// content name = My Ad +// content piece = Unknown +// content target = http://ad.example.com +``` + +A typical example for a content block that displays a text ad. + +## Tracking the content programmatically + + +There are several ways to track a content impression and/or interaction manually, semi-automatically and automatically. + +#### `trackContentImpressions()` + +You can use this method to scan the entire DOM for content blocks. +For each content block we will track a content impression immediately unless you enable `enableTrackOnlyVisbleContent` see below. + +Note: We will not send an impression of the same content block twice if you call this method multiple times unless `trackPageView()` is called meanwhile. This is useful for single page applications. The "same" content blocks means if a content block has the identical name, piece and target as an already tracked one. +Note: At this stage we do not exeute this method automatically along with a trackPageView(), we can do this later once we know it works + +#### `trackContentImpressionsWithinNode(domNode, contentTarget)` + +You can use this method if you, for instance, dynamically add an element using JavaScript to your DOM after the we have tracked the initial impressions. Calling this method will make sure an impression will be tracked for all content blocks contained within this node. + +Example +``` +var div = $('<div>...<div data-track-content>...</div>...<div data-track-content>...</div></div>'); +$('#id').append(div); + +_paq.push(['trackContentImpressionsWithinNode', div[0]]); +``` + +We would detect two new content blocks in this example. + +Please note: In case you have enabled to only track visible content blocks we will respect this. In case it contains a content block that was already tracked we will not track it again. + +#### `enableTrackOnlyVisbleContent(checkOnSroll, timeIntervalInMs)` +If you enable to track only visible content we will only track an impression if a content block is actually visible. With visible we mean the content block has been in the view port, it is actually in the DOM and is not hidden via CSS (opacity, visibility, display, ...). + +* Optionally you can tell us to rescan the DOM automatically after each scroll event by passing `checkOnSroll=true`. We will then check whether the previously hidden content blocks are visible now and if so track the impression. +* Optionally you can tell us to rescan the entire DOM for new impressions every X milliseconds by passing `timeIntervalInMs=500` (rescan DOM every 500ms). + * Note: Rescanning the entire DOM and detecting the visible state of content blocks can take a while depending on the browser and amount of content + * Note: We do not really rescan every X milliseconds. We will schedule the next rescan after a previous scan has finished. So if it takes 20ms to scan the DOM and you tell us to rescan every 50ms it can actually take 70ms. + * Note: In case your frames per second goes down you might want to increase this value +* If you do not want us to perform any checks you can either call `trackContentImpressions()` manually at any time to rescan the entire DOM or `trackContentImpressionsWithinNode()` to check only a specific part of the DOM for visible content blocks. + +It is recommended to call this method once before any call to `trackContentImpressions` or `trackContentImpressionsWithinNode()` + +#### trackContentInteractionNode(domNode, contentInteraction) + +By default we track interactions depending on a click and sometimes we cannot track interactions automatically add all. See "How do we track an interaction automatically?". In case you want to track an interaction manually for instance on a double click or on a form submit you can do this as following: + +Example +``` +anyElement.addEventListener('dblclick', function () { + _paq.push(['trackContentInteractionNode', this]); +}); +form.addEventListener('dblclick', function () { + _paq.push(['trackContentInteractionNode', this, 'submittedForm']); +}); +``` + +* The passed `domNode` can be any node within a content block or the content block element itself. Nothing will be tracked in case there is no content-block found. +* The content name and piece will be detected based on the content block +* Optionally you can set the name of the content interaction. If none is provided the content target or alternatively `Unknown will be used. +* The interaction will actually only have any effect if an impression was tracked for this content-block + +#### `trackContentImpression(contentName, contentPiece, contentTarget)` and `trackContentInteraction(contentName, contentPiece, contentInteraction)` +You should use those methods only in conjunction together. It is not recommended to use `trackContentInteraction()` after an impression was tracked automatically using on of the other methods as an interaction would only count if you do set the same content name and piece that was used to track the related impression. + +Example +``` +_paq.push(['trackContentImpression', 'Content Name', 'Content Piece', 'http://www.example.com']); + +div.addEventListener('click', function () { + _paq.push(['trackContentInteraction', 'Content Name', 'Content Piece', 'tabActivated']); +}); +``` + +Be aware that each call to one of those two methods will send one request to your Piwik tracker instance. Calling those methods too many times can cause performance problems. + +## Tracking Content Impressions API + +Content impressions are logically not really events and I don't think it makes sense to use them here. It would also make it harder to analyze events when they are mixed with pieces of content. + +* To track a content impression you will need to send the URL parameters `c_n`, `c_p` and `c_t` for name, piece and target along a tracking request. +* `c_p` for content piece and `c_t` for content target is optional. +* Multiple content impressions can be sent using bulk tracking for faster performance + +## Tracking content interactions API +Contrary to impressions, clicks are actually events and it would be nice to use events here unless it is not an outlink or download to not lose such tracking data. + +* To track a content interaction you will need to send at least the URL parameters `c_n`, `c_p` and `c_i` for name and interaction + +We will link interactions to impressions at archiver time. + +## Database + +* New column `idaction_content_url` and `idaction_content_piece` in `log_link_visit_action`. For name `idaction_name` can be reused? + +Could we also reuse `idaction_url` instead of adding new column `idaction_content_url`? +And we could also store the URL of the page showing the Content in `idaction_url_ref`. (reusing columns is good in this case) + + * Would we need a new column for each piece of content in action table to make archiver work? --> would result in many! columns + * or would we need a new table for each piece of content to make archiver work? --> would be basically a copy of the link_action table and therefore not really makes sense I reckon. Only a lot of work. Logically I am not sure if an impression is actually an "action" so it could make sense + * or would we store the pieces serialized as JSON in a `content` column? I don't know anything about the archiver but I think it wouldn't work at all + * or would we create an action entry for each piece of content? --> yes I think! + +Yes it seems most logical to create an action entry for each Content. + +## Thoughts on piwik.js +* We need to find all dom nodes having css class or html attribute. + * Options for this is traversing over each node and checking for everything -> CSS selectors cannot be used on all browsers and it might be slow therefore -> maybe lot of work to make it cross browser compatible + * https://github.com/fabiomcosta/micro-selector --> tiny selector library but does not support attributes + * http://sizzlejs.com/ Used by jQuery & co but like 30kb (compressed + gzipped 4kb). Has way too many features we don't need + * https://github.com/ded/qwery Doesn't support IE8 and a few others, no support for attribute selector + * https://github.com/padolsey/satisfy 2.4KB and probably outdated + * https://github.com/digitarald/sly very tiny and many features but last commit 3 years old + * https://github.com/alpha123/Jaguar >10KB and last commit 2 years old + * As we don't need many features we could implement it ourselves but probably needs a lot of cross-browser testing which I wanted to avoid. We'd only start with `querySelectorAll()` maybe. Brings also incredible [performance benefits](http://jsperf.com/jquery-vs-native-selector-and-element-style/2) (2-10 faster than jQuery) but there might be problems see http://stackoverflow.com/questions/11503534/jquery-vs-document-queryselectorall, http://jsfiddle.net/QdMc5/ and http://ejohn.org/blog/thoughts-on-queryselectorall/ + +## Reports +Nothing special here I think. We would probably automatically detect the type of content (image, video, text, sound, ...) depending on the content eg in case it ends with [.jpg, .png, .gif] it could be recognized as image content and show a banner in the report. + ## Open questions +* SHOULD WE ADD THE DOMAIN AND IN CASE OF A RELATIVE URL THE PATH AUTOMATICALLY TO "/" in content target? +``` + <img src="http://www.example.com/path/xyz.jpg" href="/" data-track-content /> + // content name = /path/xyz.jpg + // content piece = http://www.example.com/xyz.jpg + // content target = / + // +``` * User can decide to manually setup the proper redirect URL via piwik.php?rec=1&idsite=1&clickurl={$URL_HERE}&.... * Currently, the user would also have to add event URL parameters and make sure to send the correct name and piece to match an impression. * If the user does not use any data-content-* attributes this is very likely to fail since the auto detected content name and piece can easily change and tracking would be broken @@ -29,11 +441,16 @@ This is the technical concept for implementing content tracking. We won't plan a * If user wants to track only visible content we'll need to wait until the websites load (not DOMContentLoaded) event is triggered. Otherwise CSS might be not be applied yet and we cannot detect whether node is actually visible. Downside: Some websites might take > 10 seconds until this event is triggered. Depending on how good they are developed. During this time the user might be already no longer on that page or might have already scrolled to somewhere else. * If user wants to track all content impressions (not only the visible ones) we'd probably have to wait until at least DOMContentLoaded event is triggered * When we listen to scroll events we currently do not detect if user scrolls the entire page, not if within a div +* If the load event takes like 10 seconds later, the user has maybe already scrolled and seen some content blocks but we cannot detect... so considering viewport we need to assume all above the deepest scrollpoint was seen +* TODO we might need a trackContentImpressionsOfContentBlock() to make sure to track a content block in case we do not detect it correct that a node is visible now +* Referrer gets lost when using piwik.php +* We could have in V2 or V3 an attribute data-content-interaction="submit" to tell Piwik to listen to the submit event and to use "submit" as an interaction +* Would content impressions be tracked in overlay session? ## Answered Questions 1. Can the same content piece have different names / targets? Can the same content name have different targets/pieces? -Maybe the unique ID of a Content can be the { Content name + Content piece }. Then we would recommend users to set the same Content target for a given tuple { Content name, Content piece }. +Maybe the unique ID of a Content can be the { Content name + Content piece }. Then we would recommend users to set the same Content target for a given tuple { Content name, Content piece }. I hope it makes sense to assume this tuple will have always same Content target by design? @@ -50,7 +467,7 @@ if you have better naming idea feel free to suggest. Or maybe you have other us 3. Would a piece of content - such as a banner - have maybe custom variables etc? -It would be nice to be able to set custom variables to Contents. +It would be nice to be able to set custom variables to Contents. One possible use case is A/B testing. Maybe it would make sense to use Contents plugin for A/B testing. We could measure Content name = Experiment_TopMenu, Content piece = http://host/a.jpg. In a custom variable we would store "experiment => B". Then we would know that the given experiment is called Experiment_TopMenu and is defined by the image and that it's the variant B being served. @@ -58,12 +475,12 @@ One possible use case is A/B testing. Maybe it would make sense to use Contents Second dimension would be really powerful to have (as per suggestion in 1)). It would let user see different banner images for a given banner name. -There would be two reports: +There would be two reports: * First dimension: Banner Names, Second dimension: Banner pieces * First dimension: Banner pieces, Second dimension: Banner names (It's a bit simpler than Events because we don't need to switch the second dimension.) - + 5. I assume there can be nested content in theory. A piece of content that contains another piece of content. In this case we have to be careful when automatically picking name, target, ... Nested content makes sense (users will do this). How would it work when several contents are nested? @@ -116,169 +533,3 @@ OK 12. FYI: We need to define how a content piece is defined in markup since it can be anything (was something like piwik-banner before) see next section -## Tagging of the content piece declarative - -Namings: -* `[data-track-content] or .piwikTrackContent` == mark content element -* `[data-content-name]` == set content name -* `[data-content-piece] or .piwikContentPiece` == mark content piece element -* `[data-content-interaction] or .piwikContentInteraction` == mark click element - -### New proposed way of finding content - * Search for any `data-track-content` attribute and `piwikTrackContent` CSS class within the DOM - * One page can use both ways so we will mix the result of found contents - -### New proposed way of finding content name - -* Search for any `data-content-name` attribute within the content (`.piwikTrackContent` and children) -* Use value of content-piece in case there is one (which will discover src of image, video and audio if possible). In case it includes a domain we will remove it? TODO Maybe should remove domain only if != current location.domain -* Seach for `title` attribute in `.piwikTrackContent` -* Seach for `title` attribute in `.piwikContentPiece` -* Seach for `title` attribute in `.piwikContentInteraction` -* If neither found use "Unknown" -* Note: We will always trim the chosen name - -Matthieu feedback: - -> Use value of content-piece in case there is one - -Maybe only the path of the content-piece or even only the content-piece filename should be used? --> Yes, good one! - -> If neither found ignore content or use "Unknown" - -Before ignoring content or setting content-name to "Unknown", maybe it would be user friendly to read the first "title" attribute found in `.piwikTrackContent` (and its children). --> Not in children but otherwise yes see above! - -(But I'm not pushing this since it's not that important, if you think it's better to have only clear attribute names then I'm OK with it... As long as we document things clearly then it will be fine for users) - -TODO document how we find contentPieceNode, contentTargetNode,... - -### New proposed way of finding content target - * Search for any `data-content-interaction` attribute with a value in the content (`.piwikTrackContent` and children) - * search for `href` attribute in element with attribute `data-content-interaction` (if attribute has no value) - * search for `href` attribute in element with css class `.piwikContentInteraction` in case there is no element having attribute `data-tracking-interaction` - * search for `href` attribute in contentPieceNode -* Note: We will always trim the chosen target - -### New proposed way of finding content piece - * Search for any `data-content-piece` attribute with value within the content (`.piwikTrackContent` and children) - * if `.piwikTrackContent` is image, audio or video we will try to find a source (difficult for video) - * if `.piwikContentInteraction` is image, audio or video we will try to find a source (difficult for video) - * if `.piwikTrackContent` and `.piwikContentInteraction` is not image or video we will use `text()` of this element??? (I think we better ignore this step, text can be a lot and problems with i18n makes it not useful at all?) - * Note: source of image/video and any text() that we detect automatically can be complicated in case of i18n, it will be recommended to use data-track-content attribute -* Note: We will always trim the chosen content piece - - - -> text can be a lot and problems with i18n makes it not useful at all - -What problems are there with i18n, eg. when the page is in UTF-8? eg. when testing on google.cn to read `$('#addlang').textContent` it shows the content OK. - - -### A few Examples -``` -<img src="http://www.example.com/path/xyz.jpg" href="/" data-track-content /> -// content name = /path/xyz.jpg -// content piece = http://www.example.com/xyz.jpg -// content target = / -// TODO SHOULD WE ADD THE DOMAIN AND IN CASE OF A RELATIVE URL THE PATH AUTOMATICALLY TO "/" in content target? - -<img src="xyz.jpg" href="/" class="piwikTrackContent"/> -// content name = xyz.jpg -// content piece = xyz.jpg -// content target = / - -<img src="xyz.jpg" href="/" data-track-content data-content-name="My Content Name" data-content-piece="The Content Piece" data-content-interaction="http://www.example.com"/> -// content name = My Content Name -// content piece = The Content Piece -// content target = http://www.example.com - -<img src="xyz.jpg" href="javascript:..." data-track-content data-content-interaction="/"/> -<img src="xyz.jpg" onclick="..." data-track-content data-content-interaction="/"/> -// content name = xyz.jpg -// content piece = xyz.jpg -// content target = / - -<div data-track-content data-content-name="banner ad 1"><a href="/" data-content-interaction>click here to foo bar</a></div> -// content name = banner ad 1 -// content piece = "" -// content target = / - -<div data-track-content data-content-name="banner ad 1"><a href="/" data-content-interaction data-content-piece="xyz ad">click here to foo bar</a></div> -// content name = banner ad 1 -// content piece = xyz ad -// content target = / - -<div data-track-content data-content-name="banner ad 1" data-content-interaction="/" data-content-piece="xyz.jpg"><img src="xyz.jpg"/></div> -// content name = banner ad 1 -// content piece = xyz.jpg -// content target = / - -<div data-track-content data-content-interaction="/"><img src="xyz.jpg" data-content-piece/></div> -// content name = xyz.jpg -// content piece = xyz.jpg -// content target = / - -<div data-track-content><img src="xyz.jpg" data-content-piece/></div> -// content name = xyz.jpg -// content piece = xyz.jpg -// content target = "" - -``` - -## Tracking the impressions -Impressions are logically not really events and I don't think it makes sense to use them here. It would also make it harder to analyze events when they are mixed with pieces of content. - -* Saving in database? - * New column `idaction_content_url` and `idaction_content_piece` in `log_link_visit_action`. For name `idaction_name` can be reused? - -Could we also reuse `idaction_url` instead of adding new column `idaction_content_url`? -And we could also store the URL of the page showing the Content in `idaction_url_ref`. (reusing columns is good in this case) - - * Would we need a new column for each piece of content in action table to make archiver work? --> would result in many! columns - * or would we need a new table for each piece of content to make archiver work? --> would be basically a copy of the link_action table and therefore not really makes sense I reckon. Only a lot of work. Logically I am not sure if an impression is actually an "action" so it could make sense - * or would we store the pieces serialized as JSON in a `content` column? I don't know anything about the archiver but I think it wouldn't work at all - * or would we create an action entry for each piece of content? --> yes I think! - -Yes it seems most logical to create an action entry for each Content. - -* New Action class that handles type content -* New url parameters like `c_p`, `c_n` and `c_u` for piece of content, name and url. Maybe instead of `c_u` would be better `c_t` for target which is more generic. Sending a JSON array would not work since we cannot log multiple actions in one tracking request. They have to be sent using bulk tracking instead. - * Bulk tracking in JS is covered in: [#4910](https://github.com/piwik/piwik/issues/4910) - -* Only `c_n` would be required, `c_p` and `c_t` not as for instance a piece of content does not necessarily have a target (hard to measure a click ratio in this case?) - -OK - -## Tracking the clicks -Contrary to impressions, clicks are actually events and it would be nice to use events here. Maybe we can link an event with a piece of content? - -Linking the Click event to the piece of content will be hard, since we don't know when tracking the event, the idaction of the Content piece impression. I think only way is store in Event the two Unique IDs dimensions { Content name, Content piece }. For example the Click Event could be: - * Event Category = `Content Click` - * Event Action = `$Content_piece` - * Event Name = `$Content_Name` - -## Piwik.js -* We need to find all dom nodes having css class or html attribute. - * Options for this is traversing over each node and checking for everything -> CSS selectors cannot be used on all browsers and it might be slow therefore -> maybe lot of work to make it cross browser compatible - * https://github.com/fabiomcosta/micro-selector --> tiny selector library but does not support attributes - * http://sizzlejs.com/ Used by jQuery & co but like 30kb (compressed + gzipped 4kb). Has way too many features we don't need - * https://github.com/ded/qwery Doesn't support IE8 and a few others, no support for attribute selector - * https://github.com/padolsey/satisfy 2.4KB and probably outdated - * https://github.com/digitarald/sly very tiny and many features but last commit 3 years old - * https://github.com/alpha123/Jaguar >10KB and last commit 2 years old - * As we don't need many features we could implement it ourselves but probably needs a lot of cross-browser testing which I wanted to avoid. We'd only start with `querySelectorAll()` maybe. Brings also incredible [performance benefits](http://jsperf.com/jquery-vs-native-selector-and-element-style/2) (2-10 faster than jQuery) but there might be problems see http://stackoverflow.com/questions/11503534/jquery-vs-document-queryselectorall, http://jsfiddle.net/QdMc5/ and http://ejohn.org/blog/thoughts-on-queryselectorall/ - - -Matt: I don't have much thoughts on the JS part, besides that we don't need to have amazing support of old browsers. ie. browsers released in the last two or three years would be perfect. - -## Reports -Nothing special here I think. We would probably automatically detect the type of content (image, video, text, sound, ...) depending on the content eg in case it ends with [.jpg, .png, .gif] it could be recognized as image content and show a banner in the report. - - -## Order of implementation -Of course everything goes kinda in parallel: - -* Make tracking of impressions work -* Make a report work -* Make tracking the clicks work -* Piwik.js and tagging of the content of pieces diff --git a/plugins/Contents/API.php b/plugins/Contents/API.php index ef4bdaf781..95dc283fe1 100644 --- a/plugins/Contents/API.php +++ b/plugins/Contents/API.php @@ -49,6 +49,12 @@ class API extends \Piwik\Plugin\API $dataTable->queueFilter('ReplaceColumnNames'); $dataTable->queueFilter('ReplaceSummaryRowLabel'); + $dataTable->filter(function (DataTable $table) { + $row = $table->getRowFromLabel(Archiver::CONTENT_PIECE_NOT_SET); + if ($row) { + $row->setColumn('label', Piwik::translate('General_NotDefined', Piwik::translate('Contents_ContentPiece'))); + } + }); // Content interaction rate = interactions / impressions $dataTable->queueFilter('ColumnCallbackAddColumnPercentage', array('interaction_rate', 'nb_interactions', 'nb_impressions', $precision = 2)); diff --git a/plugins/Contents/Actions/ActionContent.php b/plugins/Contents/Actions/ActionContent.php index ecc6769813..b9edc14a29 100644 --- a/plugins/Contents/Actions/ActionContent.php +++ b/plugins/Contents/Actions/ActionContent.php @@ -28,7 +28,7 @@ class ActionContent extends Action public static function shouldHandle(Request $request) { - $name = $request->getParam('c_n'); + $name = $request->getParam('c_n'); $interaction = $request->getParam('c_i'); // if interaction is set we want it to be for instance an outlink, download, ... return !empty($name) && empty($interaction); diff --git a/plugins/Contents/Archiver.php b/plugins/Contents/Archiver.php index c5888b29a8..debac613a9 100644 --- a/plugins/Contents/Archiver.php +++ b/plugins/Contents/Archiver.php @@ -21,6 +21,7 @@ class Archiver extends \Piwik\Plugin\Archiver const CONTENTS_PIECE_NAME_RECORD_NAME = 'Contents_piece_name'; const CONTENTS_NAME_PIECE_RECORD_NAME = 'Contents_name_piece'; const CONTENT_TARGET_NOT_SET = 'Piwik_ContentTargetNotSet'; + const CONTENT_PIECE_NOT_SET = 'Piwik_ContentPieceNotSet'; /** * @var DataArray[] @@ -248,6 +249,12 @@ class Archiver extends \Piwik\Plugin\Archiver $mainDimension = $dimensions[0]; $mainLabel = $row[$mainDimension]; + // content piece is optional + if ($mainDimension == 'contentPiece' + && empty($mainLabel)) { + $mainLabel = self::CONTENT_PIECE_NOT_SET; + } + $dataArray->sumMetricsImpressions($mainLabel, $row); $this->rememberMetadataForRow($row, $mainLabel); |