diff --git a/README.rst b/README.rst
index 3fd5d4b..174d97a 100644
--- a/README.rst
+++ b/README.rst
@@ -53,6 +53,7 @@ Currently Supported Services
* `Clickmap`_ visual click tracking
* `Clicky`_ traffic analysis
* `Crazy Egg`_ visual click tracking
+* `Facebook Pixel`_ advertising analytics
* `Gaug.es`_ real time web analytics
* `Google Analytics`_ traffic analysis
* `GoSquared`_ traffic monitoring
@@ -76,6 +77,7 @@ Currently Supported Services
.. _`Clickmap`: http://getclickmap.com/
.. _`Clicky`: http://getclicky.com/
.. _`Crazy Egg`: http://www.crazyegg.com/
+.. _`Facebook Pixel`: https://developers.facebook.com/docs/facebook-pixel/
.. _`Gaug.es`: http://get.gaug.es/
.. _`Google Analytics`: http://www.google.com/analytics/
.. _`GoSquared`: http://www.gosquared.com/
diff --git a/analytical/templatetags/analytical.py b/analytical/templatetags/analytical.py
index ad4fba7..7851268 100644
--- a/analytical/templatetags/analytical.py
+++ b/analytical/templatetags/analytical.py
@@ -20,6 +20,7 @@ TAG_MODULES = [
'analytical.clickmap',
'analytical.clicky',
'analytical.crazy_egg',
+ 'analytical.facebook_pixel',
'analytical.gauges',
'analytical.google_analytics',
'analytical.gosquared',
diff --git a/analytical/templatetags/facebook_pixel.py b/analytical/templatetags/facebook_pixel.py
new file mode 100644
index 0000000..33dac0a
--- /dev/null
+++ b/analytical/templatetags/facebook_pixel.py
@@ -0,0 +1,97 @@
+"""
+Facebook Pixel template tags and filters.
+"""
+from __future__ import absolute_import
+
+import re
+
+from django.template import Library, Node, TemplateSyntaxError
+
+from analytical.utils import get_required_setting, is_internal_ip, disable_html
+
+
+FACEBOOK_PIXEL_HEAD_CODE = """\
+
+"""
+
+FACEBOOK_PIXEL_BODY_CODE = """\
+
+"""
+
+register = Library()
+
+
+def _validate_no_args(token):
+ bits = token.split_contents()
+ if len(bits) > 1:
+ raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
+
+
+@register.tag
+def facebook_pixel_head(parser, token):
+ """
+ Facebook Pixel head template tag.
+ """
+ _validate_no_args(token)
+ return FacebookPixelHeadNode()
+
+
+@register.tag
+def facebook_pixel_body(parser, token):
+ """
+ Facebook Pixel body template tag.
+ """
+ _validate_no_args(token)
+ return FacebookPixelBodyNode()
+
+
+class _FacebookPixelNode(Node):
+ """
+ Base class: override and provide code_template.
+ """
+ def __init__(self):
+ self.pixel_id = get_required_setting(
+ 'FACEBOOK_PIXEL_ID',
+ re.compile(r'^\d+$'),
+ "must be (a string containing) a number",
+ )
+
+ def render(self, context):
+ html = self.code_template % {'FACEBOOK_PIXEL_ID': self.pixel_id}
+ if is_internal_ip(context, 'FACEBOOK_PIXEL'):
+ return disable_html(html, 'Facebook Pixel')
+ else:
+ return html
+
+ @property
+ def code_template(self):
+ raise NotImplementedError # pragma: no cover
+
+
+class FacebookPixelHeadNode(_FacebookPixelNode):
+ code_template = FACEBOOK_PIXEL_HEAD_CODE
+
+
+class FacebookPixelBodyNode(_FacebookPixelNode):
+ code_template = FACEBOOK_PIXEL_BODY_CODE
+
+
+def contribute_to_analytical(add_node):
+ # ensure properly configured
+ FacebookPixelHeadNode()
+ FacebookPixelBodyNode()
+ add_node('head_bottom', FacebookPixelHeadNode)
+ add_node('body_bottom', FacebookPixelBodyNode)
diff --git a/analytical/tests/test_tag_facebook_pixel.py b/analytical/tests/test_tag_facebook_pixel.py
new file mode 100644
index 0000000..75d52aa
--- /dev/null
+++ b/analytical/tests/test_tag_facebook_pixel.py
@@ -0,0 +1,114 @@
+"""
+Tests for the Facebook Pixel template tags.
+"""
+from django.http import HttpRequest
+from django.template import Context, Template, TemplateSyntaxError
+from django.test import override_settings
+
+from analytical.templatetags.analytical import _load_template_nodes
+from analytical.templatetags.facebook_pixel import FacebookPixelHeadNode, FacebookPixelBodyNode
+from analytical.tests.utils import TagTestCase
+from analytical.utils import AnalyticalException
+
+
+expected_head_html = """\
+
+"""
+
+
+expected_body_html = """\
+
+"""
+
+
+@override_settings(FACEBOOK_PIXEL_ID='1234567890')
+class FacebookPixelTagTestCase(TagTestCase):
+
+ maxDiff = None
+
+ def test_head_tag(self):
+ html = self.render_tag('facebook_pixel', 'facebook_pixel_head')
+ self.assertEqual(expected_head_html, html)
+
+ def test_head_node(self):
+ html = FacebookPixelHeadNode().render(Context({}))
+ self.assertEqual(expected_head_html, html)
+
+ def test_body_tag(self):
+ html = self.render_tag('facebook_pixel', 'facebook_pixel_body')
+ self.assertEqual(expected_body_html, html)
+
+ def test_body_node(self):
+ html = FacebookPixelBodyNode().render(Context({}))
+ self.assertEqual(expected_body_html, html)
+
+ def test_tags_take_no_args(self):
+ self.assertRaisesRegexp(
+ TemplateSyntaxError,
+ r"^'facebook_pixel_head' takes no arguments$",
+ lambda: (Template('{% load facebook_pixel %}{% facebook_pixel_head "arg" %}')
+ .render(Context({}))),
+ )
+ self.assertRaisesRegexp(
+ TemplateSyntaxError,
+ r"^'facebook_pixel_body' takes no arguments$",
+ lambda: (Template('{% load facebook_pixel %}{% facebook_pixel_body "arg" %}')
+ .render(Context({}))),
+ )
+
+ @override_settings(FACEBOOK_PIXEL_ID=None)
+ def test_no_id(self):
+ expected_pattern = r'^FACEBOOK_PIXEL_ID setting is not set$'
+ self.assertRaisesRegexp(AnalyticalException, expected_pattern, FacebookPixelHeadNode)
+ self.assertRaisesRegexp(AnalyticalException, expected_pattern, FacebookPixelBodyNode)
+
+ @override_settings(FACEBOOK_PIXEL_ID='invalid')
+ def test_invalid_id(self):
+ expected_pattern = (
+ r"^FACEBOOK_PIXEL_ID setting: must be \(a string containing\) a number: 'invalid'$")
+ self.assertRaisesRegexp(AnalyticalException, expected_pattern, FacebookPixelHeadNode)
+ self.assertRaisesRegexp(AnalyticalException, expected_pattern, FacebookPixelBodyNode)
+
+ @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
+ def test_render_internal_ip(self):
+ request = HttpRequest()
+ request.META['REMOTE_ADDR'] = '1.1.1.1'
+ context = Context({'request': request})
+
+ def _disabled(html):
+ return '\n'.join([
+ '',
+ ])
+
+ head_html = FacebookPixelHeadNode().render(context)
+ self.assertEqual(_disabled(expected_head_html), head_html)
+
+ body_html = FacebookPixelBodyNode().render(context)
+ self.assertEqual(_disabled(expected_body_html), body_html)
+
+ def test_contribute_to_analytical(self):
+ """
+ `facebook_pixel.contribute_to_analytical` registers the head and body nodes.
+ """
+ template_nodes = _load_template_nodes()
+ self.assertEqual({
+ 'head_top': [],
+ 'head_bottom': [FacebookPixelHeadNode],
+ 'body_top': [],
+ 'body_bottom': [FacebookPixelBodyNode],
+ }, template_nodes)
diff --git a/docs/install.rst b/docs/install.rst
index defae2b..a223025 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -125,6 +125,10 @@ settings required to enable each service are listed here:
CRAZY_EGG_ACCOUNT_NUMBER = '12345678'
+* :doc:`Facebook Pixel `::
+
+ FACEBOOK_PIXEL_ID = '1234567890'
+
* :doc:`Gaug.es `::
GAUGES_SITE_ID = '0123456789abcdef0123456789abcdef'
diff --git a/docs/services/facebook_pixel.rst b/docs/services/facebook_pixel.rst
new file mode 100644
index 0000000..cfe4d21
--- /dev/null
+++ b/docs/services/facebook_pixel.rst
@@ -0,0 +1,84 @@
+=======================================
+Facebook Pixel -- advertising analytics
+=======================================
+
+`Facebook Pixel`_ is Facebook's tool for conversion tracking, optimisation and remarketing.
+
+.. _`Facebook Pixel`: https://developers.facebook.com/docs/facebook-pixel/
+
+
+.. facebook-pixel-installation:
+
+Installation
+============
+
+To start using the Facebook Pixel integration, you must have installed the
+django-analytical package and have added the ``analytical`` application
+to :const:`INSTALLED_APPS` in your project :file:`settings.py` file.
+See :doc:`../install` for details.
+
+Next you need to add the Facebook Pixel template tag to your templates.
+This step is only needed if you are not using the generic
+:ttag:`analytical.*` tags. If you are, skip to
+:ref:`facebook-pixel-configuration`.
+
+The Facebook Pixel code is inserted into templates using template tags.
+Because every page that you want to track must have the tag,
+it is useful to add it to your base template.
+At the top of the template, load the :mod:`facebook_pixel` template tag library.
+Then insert the :ttag:`facebook_pixel_head` tag at the bottom of the head section,
+and optionally insert the :ttag:`facebook_pixel_body` tag at the bottom of the body section::
+
+ {% load facebook_pixel %}
+
+
+ ...
+ {% facebook_pixel_head %}
+
+
+ ...
+ {% facebook_pixel_body %}
+
+
+
+.. note::
+ The :ttag:`facebook_pixel_body` tag code will only be used for browsers with JavaScript disabled.
+ It can be omitted if you don't need to support them.
+
+
+.. _facebook-pixel-configuration:
+
+Configuration
+=============
+
+Before you can use the Facebook Pixel integration,
+you must first set your Pixel ID.
+
+
+.. _facebook-pixel-id:
+
+Setting the Pixel ID
+--------------------
+
+Each Facebook Adverts account you have can have a Pixel ID,
+and the :mod:`facebook_pixel` tags will include it in the rendered page.
+You can find the Pixel ID on the "Pixels" section of your Facebook Adverts account.
+Set :const:`FACEBOOK_PIXEL_ID` in the project :file:`settings.py` file::
+
+ FACEBOOK_PIXEL_ID = 'XXXXXXXXXX'
+
+If you do not set a Pixel ID, the code will not be rendered.
+
+
+.. _facebook-pixel-internal-ips:
+
+Internal IP addresses
+---------------------
+
+Usually you do not want to track clicks from your development or
+internal IP addresses. By default, if the tags detect that the client
+comes from any address in the :const:`FACEBOOK_PIXEL_INTERNAL_IPS`
+setting, the tracking code is commented out. It takes the value of
+:const:`ANALYTICAL_INTERNAL_IPS` by default (which in turn is
+:const:`INTERNAL_IPS` by default). See :ref:`identifying-visitors` for
+important information about detecting the visitor IP address.