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?
 

Jeremy P

Well-known member
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.
 

Jeremy P

Well-known member
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.
 

zig

Member
@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
 

Jeremy P

Well-known member
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

zig

Member
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;
    }
 

Jeremy P

Well-known member
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
Top