XF 2.1 Adding analytics to links in email templates

Zig

Member
My team is currently working on an addon that adds analytics markers to the links in email templates. Our current solution is to use a callback in the template and pass in the URL constructed by {{ link('foo:bar', $baz) }} as a parameter. Unfortunately, this is producing some rather unexpected results. Here's an example of the code involved...

Front end code:
Code:
<xf:set var="$url">{{ link('canonical:index') }}</xf:set>
<xf:set var="$analyticsUrl"><xf:callback class="Our\Callback\Class" method="getAnalyticsLink" params="[$url, $template.title]"></xf:callback>"></xf:set>
{{ dump($url) }}
{{ dump($analyticsUrl) }}

Back end code:
Code:
    public static function getAnalyticsLink($params)
    {
        $url = $params[0];
        $templateTitle = $params[1];

        $parts = parse_url($url);

        if ($campaignName = \XF::options()->googleAnalyticsCampaignName) {
            $url = sprintf('%s://%s%s?utm_campaign=%s&utm_medium=email&utm_source=%s%s%s',
                $parts['scheme'], $parts['host'], $parts['path'],
                urlencode($campaignName), urlencode($templateTitle),
                (empty($parts['query']) ? '' : '&'.$parts['query']),
                (empty($parts['fragment']) ? '' : '#'.$parts['fragment'])
            );
        }

        return htmlspecialchars($url, ENT_QUOTES, 'UTF-8');
    }

The {{ dump($url) }} outputs exactly what you would expect: https://our.domain.name/. The {{ dump($analyticsUrl) }} however outputs ://?utm_campaign=TestCampaignName&amp;utm_medium=email&amp;utm_source=">. Since neither the original URL or the template title are making it into the callback output, it seems the parameters aren't making it to the back end at all.

Have I missed something here? Is there perhaps an easier way to achieve the desired result?
 
The method signature of your callback should be:
PHP:
public static function getAnalyticsLink(
    $contents, 
    array $params, 
    \XF\Template\Templater $templater
) {
    // ...
}

You're currently getting the contents of the callback in the $params argument, which is likely to be an empty string in this case.
 
Is there perhaps an easier way to achieve the desired result?
It might be easier to extend the mail templater \XF\Mail\Templater, and either extend the fnLink() method (responsible for handling {{ link(...) }} in templates), or swap out its $router property with a custom router where you extend the buildLink() method. Probably saves the hassle of having a bunch of template modifications.
 
@Jeremy P Thank you so much for the info! It's now working as intended. I'd assumed the callback signature for `<xf:callback>`s was only the params passed in because the documents don't mention anything about it. I'm curious how you discovered this and very thankful that you did!

I think you're correct. Extending the mail templater sounds like a much easier solution to the issue. I'll make a point to test that and see if we can get the results without having to modify all 49 email templates.

All the best!
zig
 
I'm curious how you discovered this and very thankful that you did!
I checked the documentation first and discovered the same thing you did, it doesn't mention the method signature at all. So I went digging through the code and eventually found this line in the templater:

PHP:
$output = call_user_func([$class, $method], $contents, $params, $this);
 
  • Like
Reactions: Zig
Reporting back:

Extending XF\Mail\Templater proved to be a much more expedient and (presumably) easier to maintain solution. It required very little alteration to the code listed in my original post. No template modifications are needed.

Code:
    public function fnLink($templater, &$escape, $link, $data = null, array $params = [])
    {
        $url = parent::fnLink($templater, $escape, $link, $data, $params);
        $parts = parse_url($url);

        if ($campaignName = \XF::options()->googleAnalyticsCampaignName) {
            $url = sprintf('%s://%s%s?utm_campaign=%s&utm_medium=email&utm_source=%s%s%s',
                $parts['scheme'],
                $parts['host'],
                $parts['path'],
                urlencode($campaignName),
                urlencode($this->currentTemplateName),
                (empty($parts['query']) ? '' : '&' . $parts['query']),
                (empty($parts['fragment']) ? '' : '#' . $parts['fragment'])
            );
        }

        return $url;
    }
 
For what it's worth, you could probably also append your parameters to the $params array before calling the parent for the same effect:

PHP:
public function fnLink($templater, &$escape, $link, $data = null, array $params = [])
{
    $campaign = $this->app->options()->googleAnalyticsCampaignName;
    if ($campaign) {
        $params['utm_campaign'] = $campaign;
        $params['utm_medium'] = 'email';
        $params['utm_source'] = $this->currentTemplateName;
    }

    return parent::fnLink($templater, $escape, $link, $data, $params);
}
 
  • Like
Reactions: Zig
Extending XF\Mail\Templater proved to be a much more expedient and (presumably) easier to maintain solution. It required very little alteration to the code listed in my original post. No template modifications are needed.
Did you create an add-on for this? If so, would you be willing to share?
 
Top Bottom