analytics/lib/plausible_web/plugs/tracker.ex
RobertJoonas 135471c32e
Add tagged-events script extension (#2333)
Adds a new script extension that allows tracking interactions with specific HTML elements on a website. For example - to track link clicks on one specific `<a>` element, you can tag it like this:

```html
<a href=... class="plausible-event-name=<your_event_name>">
```

And you can also tag the link with custom property names and values:

```html
<a href=... class="plausible-event-name=<your_event_name> plausible-event-<your_custom_prop>=<your_value>">
```

Tagging a link as above will send a custom event with the given name and props, if a `click` or `auxclick` browser event happens, and targets the link element.

The tracking behavior is somewhat different based on the HTML element type:  

- `<a>` 
  - triggers on `click` and `auxclick` events
  - intercepts navigation based on the same rules as `outbound-links` and `file-downloads`
- `<form>`
  - triggers on `submit` event
  - always intercepts navigation (calls `form.submit()` after preventing default and sending the Plausible event)
- other (`<img>`, `<button>`, `<span>`, `<div>`, `<h2>`, etc ...)
  - triggers on `click` and `auxclick` events
  - does not prevent default to intercept possible navigation. Simply calls Plausible with the event name and props read from the element class list.
2022-11-21 16:17:44 +02:00

77 lines
1.9 KiB
Elixir

defmodule PlausibleWeb.Tracker do
import Plug.Conn
use Agent
base_variants = [
"hash",
"outbound-links",
"exclusions",
"compat",
"local",
"manual",
"file-downloads",
"dimensions",
"tagged-events"
]
# Generates Power Set of all variants
variants =
1..Enum.count(base_variants)
|> Enum.map(fn x ->
Combination.combine(base_variants, x)
|> Enum.map(fn y -> Enum.sort(y) |> Enum.join(".") end)
end)
|> List.flatten()
@base_filenames ["plausible", "script", "analytics"]
@files_available ["plausible.js", "p.js"] ++ Enum.map(variants, fn v -> "plausible.#{v}.js" end)
def init(opts) do
Keyword.merge(opts, files_available: MapSet.new(@files_available))
end
def call(conn, files_available: files_available) do
filename =
case conn.request_path do
"/js/p.js" ->
"p.js"
"/js/" <> requested_filename ->
sorted_script_variant(requested_filename)
_ ->
nil
end
if filename && MapSet.member?(files_available, filename) do
location = Application.app_dir(:plausible, "priv/tracker/js/" <> filename)
conn
|> put_resp_header("content-type", "application/javascript")
|> put_resp_header("x-content-type-options", "nosniff")
|> put_resp_header("cross-origin-resource-policy", "cross-origin")
|> put_resp_header("access-control-allow-origin", "*")
|> put_resp_header("cache-control", "public, max-age=86400, must-revalidate")
|> send_file(200, location)
|> halt()
else
conn
end
end
defp sorted_script_variant(requested_filename) do
case String.split(requested_filename, ".") do
[base_filename | rest] when base_filename in @base_filenames ->
sorted_variants =
rest
|> List.delete("js")
|> Enum.sort()
Enum.join(["plausible"] ++ sorted_variants ++ ["js"], ".")
_ ->
nil
end
end
end