Skip to content

Commit 9ac62d3

Browse files
committed
Introduce analytics pixels infrastructure
1 parent e88e25a commit 9ac62d3

File tree

14 files changed

+572
-24
lines changed

14 files changed

+572
-24
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
$hackageCssTheme()$
5+
<title>Analytics pixels for $pkgname$ | Hackage</title>
6+
</head>
7+
8+
<body>
9+
$hackagePageHeader()$
10+
11+
<div id="content">
12+
13+
<h2>Adding a analytics pixel to <a href="/package/$pkgname$">$pkgname$</a></h2>
14+
15+
<p>
16+
Configure an analytics pixel to be automatically loaded on your package’s page on Hackage.
17+
You’ll need an image URL from any external analytics provider, e.g. <a href="about.scarf.sh">Scarf</a>, which is provided
18+
for free and can surface information about web traffic to your package including geographic
19+
distribution, version distribution, and companies.
20+
</p>
21+
22+
<form method="POST" class="box" action="/package/$pkgname$/analytics-pixels">
23+
<label for="analytics-pixel">Analytics Image URL</label>
24+
<input name="analytics-pixel" type="text" />
25+
<input type="submit" />
26+
</form>
27+
28+
<h2>Existing analytics pixels for <a href="/package/$pkgname$">$pkgname$</a></h2>
29+
30+
<ul>
31+
$analyticsPixels:{analyticsPixel|
32+
<li>
33+
<form method="POST" action="/package/$pkgname$/analytics-pixels">
34+
<label for="analytics-pixel">$analyticsPixel$</label>
35+
<input type="hidden" name="analytics-pixel" value="$analyticsPixel$"/>
36+
<input type="hidden" name="_method" value="DELETE" />
37+
<input type="submit" value="Delete" />
38+
</form>
39+
</li>
40+
}; separator=""$
41+
</ul>
42+
43+
</div>
44+
</body>
45+
</html>

datafiles/templates/Html/package-page.html.st

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@
9595
edit package information
9696
</a>
9797
</li>
98+
<li>
99+
<a href="$baseurl$/package/$package.name$/analytics-pixels">
100+
edit package analytics pixels
101+
</a>
102+
</li>
98103
</ul>
99104
<p>Candidates</p>
100105
<ul>
@@ -272,5 +277,10 @@
272277
<script src="$doc.baseUrl$/quick-jump.min.js" type="text/javascript"></script>
273278
<script type="text/javascript"> quickNav.init("$doc.baseUrl$", function(toggle) {var t = document.getElementById('quickjump-trigger');if (t) {t.onclick = function(e) { e.preventDefault(); toggle(); };}}); </script>
274279
$endif$
280+
281+
$analyticsPixels:{analyticsPixelUrl|
282+
<img referrerpolicy="no-referrer-when-downgrade" src="$analyticsPixelUrl$" />
283+
}; separator=""$
284+
275285
</body>
276286
</html>
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
$hackageCssTheme()$
5+
<title>Analytics pixels for all of $username$'s packages | Hackage</title>
6+
</head>
7+
8+
<body>
9+
$hackagePageHeader()$
10+
11+
<div id="content">
12+
13+
<h2>Create a analytics pixel</h2>
14+
15+
<p>
16+
Configure an analytics pixel to be automatically loaded on your package’s page on Hackage.
17+
You’ll need an image URL from any external analytics provider, e.g. <a href="about.scarf.sh">Scarf</a>, which is provided
18+
for free and can surface information about web traffic to your package including geographic
19+
distribution, version distribution, and companies.
20+
</p>
21+
22+
<form method="POST" class="box" action="/user/$username$/analytics-pixels">
23+
<p>
24+
<label for="package">Package</label>
25+
<select name="package">
26+
$pkgs:{pkg|
27+
<option value="$pkg$">$pkg$</option>
28+
}; separator=""$
29+
</select>
30+
</p>
31+
<p>
32+
<label for="analytics-pixel">Analytics pixel URL</label>
33+
<input name="analytics-pixel" type="text" />
34+
</p>
35+
<input type="submit" />
36+
</form>
37+
38+
<h2>Existing analytics pixels</h2>
39+
40+
$pkgpixels:{pkgpixel|
41+
<h3><a href="/package/$pkgpixel.0$">$pkgpixel.0$</a></h3>
42+
<ul>
43+
$pkgpixel.1:{analyticsPixel|
44+
<li>
45+
<form method="POST" action="/user/$username$/analytics-pixels">
46+
<label for="analytics-pixel">$analyticsPixel$</label>
47+
<input type="hidden" name="package" value="$pkgpixel.0$" />
48+
<input type="hidden" name="analytics-pixel" value="$analyticsPixel$"/>
49+
<input type="hidden" name="_method" value="DELETE" />
50+
<input type="submit" value="Delete" />
51+
<form>
52+
</li>
53+
}; separator=""$
54+
</ul>
55+
}; separator=""$
56+
57+
</div>
58+
</body>
59+
</html>

datafiles/templates/Users/manage.html.st

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ $tokens:{token|
6464
<li>
6565
<a href="/user/$username$/password">Change your password</a>
6666
</li>
67+
<li>
68+
<a href="/user/$username$/analytics-pixels">Analytics pixels</a>
69+
</li>
6770
</ul>
6871

6972
</div>

hackage-server.cabal

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,8 @@ library lib-server
343343
Distribution.Server.Features.Tags
344344
Distribution.Server.Features.Tags.Backup
345345
Distribution.Server.Features.Tags.State
346+
Distribution.Server.Features.AnalyticsPixels
347+
Distribution.Server.Features.AnalyticsPixels.State
346348
Distribution.Server.Features.UserDetails
347349
Distribution.Server.Features.UserSignup
348350
Distribution.Server.Features.StaticFiles

src/Distribution/Server/Features.hs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import Distribution.Server.Features.PreferredVersions (initVersionsFeature)
3636
-- [reverse index disabled] import Distribution.Server.Features.ReverseDependencies (initReverseFeature)
3737
import Distribution.Server.Features.DownloadCount (initDownloadFeature)
3838
import Distribution.Server.Features.Tags (initTagsFeature)
39+
import Distribution.Server.Features.AnalyticsPixels (initAnalyticsPixelsFeature)
3940
import Distribution.Server.Features.Search (initSearchFeature)
4041
import Distribution.Server.Features.PackageList (initListFeature)
4142
import Distribution.Server.Features.HaskellPlatform (initPlatformFeature)
@@ -127,6 +128,8 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do
127128
initDownloadFeature env
128129
mkTagsFeature <- logStartup "tags" $
129130
initTagsFeature env
131+
mkAnalyticsPixelsFeature <- logStartup "analytics pixels" $
132+
initAnalyticsPixelsFeature env
130133
mkVersionsFeature <- logStartup "versions" $
131134
initVersionsFeature env
132135
-- mkReverseFeature <- logStartup "reverse deps" $
@@ -255,6 +258,11 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do
255258
uploadFeature
256259
usersFeature
257260

261+
analyticsPixelsFeature <- mkAnalyticsPixelsFeature
262+
coreFeature
263+
usersFeature
264+
uploadFeature
265+
258266
versionsFeature <- mkVersionsFeature
259267
coreFeature
260268
uploadFeature
@@ -292,6 +300,7 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do
292300
versionsFeature
293301
-- [reverse index disabled] reverseFeature
294302
tagsFeature
303+
analyticsPixelsFeature
295304
downloadFeature
296305
votesFeature
297306
listFeature
@@ -371,6 +380,7 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do
371380
, getFeatureInterface documentationCandidatesFeature
372381
, getFeatureInterface downloadFeature
373382
, getFeatureInterface tagsFeature
383+
, getFeatureInterface analyticsPixelsFeature
374384
, getFeatureInterface versionsFeature
375385
-- [reverse index disabled] , getFeatureInterface reverseFeature
376386
, getFeatureInterface searchFeature
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
{-# LANGUAGE RankNTypes, NamedFieldPuns, RecordWildCards #-}
2+
3+
-- | Implements a system to allow users to upvote packages.
4+
--
5+
module Distribution.Server.Features.AnalyticsPixels
6+
( AnalyticsPixelsFeature(..)
7+
, AnalyticsPixel(..)
8+
, initAnalyticsPixelsFeature
9+
) where
10+
11+
import Data.Set (Set)
12+
13+
import Distribution.Server.Features.AnalyticsPixels.State
14+
15+
import Distribution.Server.Framework
16+
import Distribution.Server.Framework.BackupRestore
17+
18+
import Distribution.Server.Features.Core
19+
import Distribution.Server.Features.Upload
20+
import Distribution.Server.Features.Users
21+
22+
import Distribution.Package
23+
24+
-- | Define the prototype for this feature
25+
data AnalyticsPixelsFeature = AnalyticsPixelsFeature {
26+
analyticsPixelsFeatureInterface :: HackageFeature,
27+
analyticsPixelsResource :: Resource,
28+
userAnalyticsPixelsResource :: Resource,
29+
30+
analyticsPixelAdded :: Hook (PackageName, AnalyticsPixel) (),
31+
analyticsPixelRemoved :: Hook (PackageName, AnalyticsPixel) (),
32+
33+
-- | Returns all 'AnalyticsPixel's associated with a 'Package'.
34+
getPackageAnalyticsPixels :: forall m. MonadIO m => PackageName -> m (Set AnalyticsPixel),
35+
36+
-- | Adds a new 'AnalyticsPixel' to a 'Package'. Returns True in case it was added. False in case
37+
-- it's already existing.
38+
addPackageAnalyticsPixel :: forall m. MonadIO m => PackageName -> AnalyticsPixel -> m Bool,
39+
40+
-- | Remove a 'AnalyticsPixel' from a 'Package'.
41+
removePackageAnalyticsPixel :: forall m. MonadIO m => PackageName -> AnalyticsPixel -> m ()
42+
}
43+
44+
-- | Implement the isHackageFeature 'interface'
45+
instance IsHackageFeature AnalyticsPixelsFeature where
46+
getFeatureInterface = analyticsPixelsFeatureInterface
47+
48+
-- | Called from Features.hs to initialize this feature
49+
initAnalyticsPixelsFeature :: ServerEnv
50+
-> IO ( CoreFeature
51+
-> UserFeature
52+
-> UploadFeature
53+
-> IO AnalyticsPixelsFeature)
54+
initAnalyticsPixelsFeature env@ServerEnv{serverStateDir} = do
55+
dbAnalyticsPixelsState <- analyticsPixelsStateComponent serverStateDir
56+
analyticsPixelAdded <- newHook
57+
analyticsPixelRemoved <- newHook
58+
59+
return $ \coref@CoreFeature{..} userf@UserFeature{..} uploadf -> do
60+
let feature = analyticsPixelsFeature env
61+
dbAnalyticsPixelsState
62+
coref userf uploadf analyticsPixelAdded analyticsPixelRemoved
63+
64+
return feature
65+
66+
-- | Define the backing store (i.e. database component)
67+
analyticsPixelsStateComponent :: FilePath -> IO (StateComponent AcidState AnalyticsPixelsState)
68+
analyticsPixelsStateComponent stateDir = do
69+
st <- openLocalStateFrom (stateDir </> "db" </> "AnalyticsPixels") initialAnalyticsPixelsState
70+
return StateComponent {
71+
stateDesc = "Backing store for AnalyticsPixels feature"
72+
, stateHandle = st
73+
, getState = query st GetAnalyticsPixelsState
74+
, putState = update st . ReplaceAnalyticsPixelsState
75+
, resetState = analyticsPixelsStateComponent
76+
, backupState = \_ _ -> []
77+
, restoreState = RestoreBackup {
78+
restoreEntry = error "Unexpected backup entry"
79+
, restoreFinalize = return initialAnalyticsPixelsState
80+
}
81+
}
82+
83+
84+
-- | Default constructor for building this feature.
85+
analyticsPixelsFeature :: ServerEnv
86+
-> StateComponent AcidState AnalyticsPixelsState
87+
-> CoreFeature -- To get site package list
88+
-> UserFeature -- To authenticate users
89+
-> UploadFeature -- For accessing package maintainers and trustees
90+
-> Hook (PackageName, AnalyticsPixel) () -- Signals addition of a new AnalyticsPixel
91+
-> Hook (PackageName, AnalyticsPixel) () -- Signals removeal of a AnalyticsPixel
92+
-> AnalyticsPixelsFeature
93+
94+
analyticsPixelsFeature ServerEnv{..}
95+
analyticsPixelsState
96+
CoreFeature { coreResource = CoreResource{..} }
97+
UserFeature{..}
98+
UploadFeature{..}
99+
analyticsPixelAdded
100+
analyticsPixelRemoved
101+
= AnalyticsPixelsFeature {..}
102+
where
103+
analyticsPixelsFeatureInterface = (emptyHackageFeature "AnalyticsPixels") {
104+
featureDesc = "Allow users to attach analytics pixels to their packages",
105+
featureResources = [analyticsPixelsResource, userAnalyticsPixelsResource]
106+
, featureState = [abstractAcidStateComponent analyticsPixelsState]
107+
}
108+
109+
analyticsPixelsResource :: Resource
110+
analyticsPixelsResource = resourceAt "/package/:package/analytics-pixels.:format"
111+
112+
userAnalyticsPixelsResource :: Resource
113+
userAnalyticsPixelsResource = resourceAt "/user/:username/analytics-pixels.:format"
114+
115+
getPackageAnalyticsPixels :: MonadIO m => PackageName -> m (Set AnalyticsPixel)
116+
getPackageAnalyticsPixels name =
117+
queryState analyticsPixelsState (AnalyticsPixelsForPackage name)
118+
119+
addPackageAnalyticsPixel :: MonadIO m => PackageName -> AnalyticsPixel -> m Bool
120+
addPackageAnalyticsPixel name pixel = do
121+
added <- updateState analyticsPixelsState (AddPackageAnalyticsPixel name pixel)
122+
when added $ runHook_ analyticsPixelAdded (name, pixel)
123+
pure added
124+
125+
removePackageAnalyticsPixel :: MonadIO m => PackageName -> AnalyticsPixel -> m ()
126+
removePackageAnalyticsPixel name pixel = do
127+
updateState analyticsPixelsState (RemovePackageAnalyticsPixel name pixel)
128+
runHook_ analyticsPixelRemoved (name, pixel)

0 commit comments

Comments
 (0)