diff --git a/lib/analytics/plausible.dart b/lib/analytics/plausible.dart
new file mode 100644
index 00000000..027bc5d2
--- /dev/null
+++ b/lib/analytics/plausible.dart
@@ -0,0 +1,44 @@
+import 'dart:convert';
+
+class PlausibleEvent {
+  String eventName = "";
+  String href = "";
+  String hostname = "";
+
+  String referrer = "";
+  int innerWidth = 0;
+  bool hash = false;
+  Map<String, dynamic> props = {};
+
+  static final _acceptedPayloadTypes = [int, double, String, bool];
+  Map<String, dynamic> toMap() {
+    assert(() {
+      for (var entry in props.entries) {
+        var type = entry.value.runtimeType;
+        if (!_acceptedPayloadTypes.contains(type)) {
+          return false;
+        }
+      }
+      return true;
+    }(), "Plausible Event Payload contains unsupported type");
+
+    return {
+      'n': eventName,
+      'u': href,
+      'd': hostname,
+      'r': referrer,
+      'w': innerWidth,
+      'h': hash ? 1 : 0,
+      'p': json.encode(props),
+    };
+  }
+
+  String toJson() => json.encode(toMap());
+}
+
+/*
+const req = new XMLHttpRequest();
+  req.open('POST', `${data.apiHost}/api/event`, true);
+  req.setRequestHeader('Content-Type', 'text/plain');
+  req.send(JSON.stringify(payload));
+  */
diff --git a/test/analytics/plausible.dart b/test/analytics/plausible.dart
new file mode 100644
index 00000000..57e26561
--- /dev/null
+++ b/test/analytics/plausible.dart
@@ -0,0 +1,31 @@
+import 'package:test/test.dart';
+
+import 'package:gitjournal/analytics/plausible.dart';
+
+final Matcher throwsAssertionError = throwsA(isA<AssertionError>());
+
+void main() {
+  test('Serialization', () {
+    var e = PlausibleEvent();
+    e.props = {
+      'a': 1,
+      'b': 5.5,
+      'c': false,
+      'd': 'Hello',
+    };
+
+    expect(e.toJson(), isNotEmpty);
+  });
+
+  test('Invalid Serialization', () {
+    var fn = () {
+      var e = PlausibleEvent();
+      e.props = {
+        'a': [1],
+      };
+      e.toJson();
+    };
+
+    expect(fn, throwsAssertionError);
+  });
+}