Repository: snowplow/snowplow-python-tracker

Issue:
Refactor track_xxx() methods 

Discussion:

Patch:
diff --git a/examples/redis_example/redis_app.py b/examples/redis_example/redis_app.py
index a4ac1e7..553a547 100644
--- a/examples/redis_example/redis_app.py
+++ b/examples/redis_example/redis_app.py
@@ -1,4 +1,12 @@
-from snowplow_tracker import Tracker
+from snowplow_tracker import (
+    Tracker,
+    ScreenView,
+    PagePing,
+    PageView,
+    SelfDescribing,
+    StructuredEvent,
+    SelfDescribingJson,
+)
 from snowplow_tracker.typing import PayloadDict
 import json
 import redis
@@ -51,9 +59,28 @@ def main():
 
     t = Tracker(namespace="snowplow_tracker", emitters=emitter)
 
-    t.track_page_view("https://www.snowplow.io", "Homepage")
-    t.track_page_ping("https://www.snowplow.io", "Homepage")
-    t.track_link_click("https://www.snowplow.io")
+    page_view = PageView(page_url="https://www.snowplow.io", page_title="Homepage")
+    t.track(page_view)
+
+    page_ping = PagePing(page_url="https://www.snowplow.io", page_title="Homepage")
+    t.track(page_ping)
+
+    link_click = SelfDescribing(
+        SelfDescribingJson(
+            "iglu:com.snowplowanalytics.snowplow/link_click/jsonschema/1-0-1",
+            {"targetUrl": "https://www.snowplow.io"},
+        )
+    )
+    t.track(link_click)
+
+    id = t.get_uuid()
+    screen_view = ScreenView(id_=id, name="name")
+    t.track(screen_view)
+
+    struct_event = StructuredEvent(
+        category="shop", action="add-to-basket", property_="pcs", value=2
+    )
+    t.track(struct_event)
 
 
 if __name__ == "__main__":
diff --git a/examples/snowplow_api_example/snowplow_app.py b/examples/snowplow_api_example/snowplow_app.py
index 9cc9f09..1bbd21c 100644
--- a/examples/snowplow_api_example/snowplow_app.py
+++ b/examples/snowplow_api_example/snowplow_app.py
@@ -5,6 +5,11 @@ from snowplow_tracker import (
     Subject,
     TrackerConfiguration,
     SelfDescribingJson,
+    PagePing,
+    PageView,
+    ScreenView,
+    SelfDescribing,
+    StructuredEvent,
 )
 
 
@@ -15,11 +20,12 @@ def get_url_from_args():
 
 
 def main():
-
     collector_url = get_url_from_args()
     # Configure Emitter
     custom_retry_codes = {500: False, 401: True}
-    emitter_config = EmitterConfiguration(batch_size=5, custom_retry_codes=custom_retry_codes)
+    emitter_config = EmitterConfiguration(
+        batch_size=5, custom_retry_codes=custom_retry_codes
+    )
 
     # Configure Tracker
     tracker_config = TrackerConfiguration(encode_base64=True)
@@ -39,19 +45,28 @@ def main():
 
     tracker = Snowplow.get_tracker("ns")
 
-    tracker.track_page_view("https://www.snowplow.io", "Homepage")
-    tracker.track_page_ping("https://www.snowplow.io", "Homepage")
-    tracker.track_link_click("https://www.snowplow.io/about")
-    tracker.track_page_view("https://www.snowplow.io/about", "About")
+    page_view = PageView(page_url="https://www.snowplow.io", page_title="Homepage")
+    tracker.track(page_view)
+
+    page_ping = PagePing(page_url="https://www.snowplow.io", page_title="Homepage")
+    tracker.track(page_ping)
 
-    tracker.track_self_describing_event(
+    link_click = SelfDescribing(
         SelfDescribingJson(
             "iglu:com.snowplowanalytics.snowplow/link_click/jsonschema/1-0-1",
-            {"targetUrl": "example.com"},
+            {"targetUrl": "https://www.snowplow.io"},
         )
     )
-    tracker.track_struct_event("shop", "add-to-basket", None, "pcs", 2)
+    tracker.track(link_click)
 
+    id = tracker.get_uuid()
+    screen_view = ScreenView(id_=id, name="name")
+    tracker.track(screen_view)
+
+    struct_event = StructuredEvent(
+        category="shop", action="add-to-basket", property_="pcs", value=2
+    )
+    tracker.track(struct_event)
     tracker.flush()
 
 
diff --git a/examples/tracker_api_example/app.py b/examples/tracker_api_example/app.py
index 2483d50..3777a59 100644
--- a/examples/tracker_api_example/app.py
+++ b/examples/tracker_api_example/app.py
@@ -4,6 +4,11 @@ from snowplow_tracker import (
     Emitter,
     Subject,
     SelfDescribingJson,
+    PageView,
+    PagePing,
+    SelfDescribing,
+    ScreenView,
+    StructuredEvent,
 )
 import sys
 
@@ -26,17 +31,41 @@ def main():
 
     print("Sending events to " + e.endpoint)
 
-    t.track_page_view("https://www.snowplow.io", "Homepage")
-    t.track_page_ping("https://www.snowplow.io", "Homepage")
-    t.track_link_click("https://www.snowplow.io")
+    page_view = PageView(
+        page_url="https://www.snowplow.io",
+        page_title="Homepage",
+        event_subject=t.subject,
+    )
+    t.track(page_view)
+
+    page_ping = PagePing(
+        page_url="https://www.snowplow.io",
+        page_title="Homepage",
+        event_subject=t.subject,
+    )
+    t.track(page_ping)
 
-    t.track_self_describing_event(
+    link_click = SelfDescribing(
         SelfDescribingJson(
             "iglu:com.snowplowanalytics.snowplow/link_click/jsonschema/1-0-1",
-            {"targetUrl": "example.com"},
-        )
+            {"targetUrl": "https://www.snowplow.io"},
+        ),
+        event_subject=t.subject,
+    )
+    t.track(link_click)
+
+    id = t.get_uuid()
+    screen_view = ScreenView(id_=id, name="name", event_subject=t.subject)
+    t.track(screen_view)
+
+    struct_event = StructuredEvent(
+        category="shop",
+        action="add-to-basket",
+        property_="pcs",
+        value=2,
+        event_subject=t.subject,
     )
-    t.track_struct_event("shop", "add-to-basket", None, "pcs", 2)
+    t.track(struct_event)
     t.flush()
 
 
diff --git a/setup.py b/setup.py
index 2510efa..b67c8f3 100644
--- a/setup.py
+++ b/setup.py
@@ -36,10 +36,7 @@ setup(
     version="0.15.0",
     author=authors_str,
     author_email=authors_email_str,
-    packages=[
-        "snowplow_tracker",
-        "snowplow_tracker.test",
-    ],
+    packages=["snowplow_tracker", "snowplow_tracker.test", "snowplow_tracker.events"],
     url="http://snowplow.io",
     license="Apache License 2.0",
     description="Snowplow event tracker for Python. Add analytics to your Python and Django apps, webapps and games",
diff --git a/snowplow_tracker/__init__.py b/snowplow_tracker/__init__.py
index 8745b37..689b253 100644
--- a/snowplow_tracker/__init__.py
+++ b/snowplow_tracker/__init__.py
@@ -8,3 +8,11 @@ from snowplow_tracker.tracker_configuration import TrackerConfiguration
 from snowplow_tracker.snowplow import Snowplow
 from snowplow_tracker.contracts import disable_contracts, enable_contracts
 from snowplow_tracker.event_store import EventStore
+from snowplow_tracker.events import (
+    Event,
+    PageView,
+    PagePing,
+    SelfDescribing,
+    StructuredEvent,
+    ScreenView,
+)
diff --git a/snowplow_tracker/constants.py b/snowplow_tracker/constants.py
new file mode 100644
index 0000000..579ff86
--- /dev/null
+++ b/snowplow_tracker/constants.py
@@ -0,0 +1,27 @@
+# """
+#     constants.py
+
+#     Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+
+#     This program is licensed to you under the Apache License Version 2.0,
+#     and you may not use this file except in compliance with the Apache License
+#     Version 2.0. You may obtain a copy of the Apache License Version 2.0 at
+#     http://www.apache.org/licenses/LICENSE-2.0.
+
+#     Unless required by applicable law or agreed to in writing,
+#     software distributed under the Apache License Version 2.0 is distributed on
+#     an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+#     express or implied. See the Apache License Version 2.0 for the specific
+#     language governing permissions and limitations there under.
+# """
+from typing import List
+from snowplow_tracker import _version, SelfDescribingJson
+
+VERSION = "py-%s" % _version.__version__
+DEFAULT_ENCODE_BASE64 = True
+BASE_SCHEMA_PATH = "iglu:com.snowplowanalytics.snowplow"
+MOBILE_SCHEMA_PATH = "iglu:com.snowplowanalytics.mobile"
+SCHEMA_TAG = "jsonschema"
+CONTEXT_SCHEMA = "%s/contexts/%s/1-0-1" % (BASE_SCHEMA_PATH, SCHEMA_TAG)
+UNSTRUCT_EVENT_SCHEMA = "%s/unstruct_event/%s/1-0-0" % (BASE_SCHEMA_PATH, SCHEMA_TAG)
+ContextArray = List[SelfDescribingJson]
diff --git a/snowplow_tracker/events/__init__.py b/snowplow_tracker/events/__init__.py
new file mode 100644
index 0000000..0f75c84
--- /dev/null
+++ b/snowplow_tracker/events/__init__.py
@@ -0,0 +1,22 @@
+# """
+#     __init__.py
+
+#     Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+
+#     This program is licensed to you under the Apache License Version 2.0,
+#     and you may not use this file except in compliance with the Apache License
+#     Version 2.0. You may obtain a copy of the Apache License Version 2.0 at
+#     http://www.apache.org/licenses/LICENSE-2.0.
+
+#     Unless required by applicable law or agreed to in writing,
+#     software distributed under the Apache License Version 2.0 is distributed on
+#     an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+#     express or implied. See the Apache License Version 2.0 for the specific
+#     language governing permissions and limitations there under.
+# """
+from snowplow_tracker.events.event import Event
+from snowplow_tracker.events.page_ping import PagePing
+from snowplow_tracker.events.page_view import PageView
+from snowplow_tracker.events.self_describing import SelfDescribing
+from snowplow_tracker.events.structured_event import StructuredEvent
+from snowplow_tracker.events.screen_view import ScreenView
diff --git a/snowplow_tracker/events/event.py b/snowplow_tracker/events/event.py
new file mode 100644
index 0000000..7f510ba
--- /dev/null
+++ b/snowplow_tracker/events/event.py
@@ -0,0 +1,134 @@
+# """
+#     event.py
+
+#     Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+
+#     This program is licensed to you under the Apache License Version 2.0,
+#     and you may not use this file except in compliance with the Apache License
+#     Version 2.0. You may obtain a copy of the Apache License Version 2.0 at
+#     http://www.apache.org/licenses/LICENSE-2.0.
+
+#     Unless required by applicable law or agreed to in writing,
+#     software distributed under the Apache License Version 2.0 is distributed on
+#     an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+#     express or implied. See the Apache License Version 2.0 for the specific
+#     language governing permissions and limitations there under.
+# """
+
+from typing import Optional, List
+from snowplow_tracker import payload
+from snowplow_tracker.subject import Subject
+
+from snowplow_tracker.self_describing_json import SelfDescribingJson
+
+from snowplow_tracker.constants import CONTEXT_SCHEMA
+from snowplow_tracker.typing import JsonEncoderFunction, PayloadDict
+
+
+class Event(object):
+    """
+    Event class which contains
+    elements that can be set in all events. These are context, trueTimestamp, and Subject.
+
+    Context is a list of custom SelfDescribingJson entities.
+    TrueTimestamp is a user-defined timestamp.
+    Subject is an event-specific Subject. Its fields will override those of the
+    Tracker-associated Subject, if present.
+
+    """
+
+    def __init__(
+        self,
+        dict_: Optional[PayloadDict] = None,
+        event_subject: Optional[Subject] = None,
+        context: Optional[List[SelfDescribingJson]] = None,
+        true_timestamp: Optional[float] = None,
+    ) -> None:
+        """
+        Constructor
+        :param  dict_:              Optional Dictionary to be added to the Events Payload
+        :type   dict_:              dict(string:\\*) | None
+        :param  event_subject:      Optional per event subject
+        :type   event_subject:      subject | None
+        :param  context:            Custom context for the event
+        :type   context:            context_array | None
+        :param  true_timestamp:     Optional event timestamp in milliseconds
+        :type   true_timestamp:     int | float | None
+
+        """
+        self.payload = payload.Payload(dict_=dict_)
+        self.event_subject = event_subject
+        self.context = context or []
+        self.true_timestamp = true_timestamp
+
+    def build_payload(
+        self,
+        encode_base64: bool,
+        json_encoder: Optional[JsonEncoderFunction],
+        subject: Optional[Subject] = None,
+    ) -> "payload.Payload":
+        """
+        :param encode_base64:    Whether JSONs in the payload should be base-64 encoded
+        :type  encode_base64:    bool
+        :param json_encoder:     Custom JSON serializer that gets called on non-serializable object
+        :type  json_encoder:     function | None
+        :param  subject:         Optional per event subject
+        :type   subject:         subject | None
+        :rtype:                  payload.Payload
+        """
+        if len(self.context) > 0:
+            context_jsons = list(map(lambda c: c.to_json(), self.context))
+            context_envelope = SelfDescribingJson(
+                CONTEXT_SCHEMA, context_jsons
+            ).to_json()
+            self.payload.add_json(
+                context_envelope, encode_base64, "cx", "co", json_encoder
+            )
+
+        if isinstance(
+            self.true_timestamp,
+            (
+                int,
+                float,
+            ),
+        ):
+            self.payload.add("ttm", int(self.true_timestamp))
+
+        fin_subject = self.event_subject if self.event_subject is not None else subject
+
+        if fin_subject is not None:
+            self.payload.add_dict(fin_subject.standard_nv_pairs)
+        return self.payload
+
+    @property
+    def event_subject(self) -> Optional[Subject]:
+        """
+        Optional per event subject
+        """
+        return self._event_subject
+
+    @event_subject.setter
+    def event_subject(self, value: Optional[Subject]):
+        self._event_subject = value
+
+    @property
+    def context(self) -> List[SelfDescribingJson]:
+        """
+        Custom context for the event
+        """
+        return self._context
+
+    @context.setter
+    def context(self, value: List[SelfDescribingJson]):
+        self._context = value
+
+    @property
+    def true_timestamp(self) -> Optional[float]:
+        """
+        Optional event timestamp in milliseconds
+        """
+        return self._true_timestamp
+
+    @true_timestamp.setter
+    def true_timestamp(self, value: Optional[float]):
+        self._true_timestamp = value
diff --git a/snowplow_tracker/events/page_ping.py b/snowplow_tracker/events/page_ping.py
new file mode 100644
index 0000000..b008479
--- /dev/null
+++ b/snowplow_tracker/events/page_ping.py
@@ -0,0 +1,155 @@
+# """
+#     page_ping.py
+
+#     Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+
+#     This program is licensed to you under the Apache License Version 2.0,
+#     and you may not use this file except in compliance with the Apache License
+#     Version 2.0. You may obtain a copy of the Apache License Version 2.0 at
+#     http://www.apache.org/licenses/LICENSE-2.0.
+
+#     Unless required by applicable law or agreed to in writing,
+#     software distributed under the Apache License Version 2.0 is distributed on
+#     an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+#     express or implied. See the Apache License Version 2.0 for the specific
+#     language governing permissions and limitations there under.
+# """
+from snowplow_tracker.events.event import Event
+from typing import Optional, List
+from snowplow_tracker.self_describing_json import SelfDescribingJson
+from snowplow_tracker.subject import Subject
+from snowplow_tracker.contracts import non_empty_string
+
+
+class PagePing(Event):
+    """
+    Constructs a PagePing event object.
+
+    When tracked, generates a "pp" or "page_ping" event.
+
+    """
+
+    def __init__(
+        self,
+        page_url: str,
+        page_title: Optional[str] = None,
+        referrer: Optional[str] = None,
+        min_x: Optional[int] = None,
+        max_x: Optional[int] = None,
+        min_y: Optional[int] = None,
+        max_y: Optional[int] = None,
+        event_subject: Optional[Subject] = None,
+        context: Optional[List[SelfDescribingJson]] = None,
+        true_timestamp: Optional[float] = None,
+    ) -> None:
+        """
+        :param  page_url:       URL of the viewed page
+        :type   page_url:       non_empty_string
+        :param  page_title:     Title of the viewed page
+        :type   page_title:     string_or_none
+        :param  referrer:       Referrer of the page
+        :type   referrer:       string_or_none
+        :param  min_x:          Minimum page x offset seen in the last ping period
+        :type   min_x:          int | None
+        :param  max_x:          Maximum page x offset seen in the last ping period
+        :type   max_x:          int | None
+        :param  min_y:          Minimum page y offset seen in the last ping period
+        :type   min_y:          int | None
+        :param  max_y:          Maximum page y offset seen in the last ping period
+        :type   max_y:          int | None
+        :param  event_subject:   Optional per event subject
+        :type   event_subject:   subject | None
+        :param  context:         Custom context for the event
+        :type   context:         context_array | None
+        :param  true_timestamp:          Optional event timestamp in milliseconds
+        :type   true_timestamp:          int | float | None
+        """
+        super(PagePing, self).__init__(
+            event_subject=event_subject, context=context, true_timestamp=true_timestamp
+        )
+        self.payload.add("e", "pp")
+        self.page_url = page_url
+        self.page_title = page_title
+        self.referrer = referrer
+        self.min_x = min_x
+        self.max_x = max_x
+        self.min_y = min_y
+        self.max_y = max_y
+
+    @property
+    def page_url(self) -> str:
+        """
+        URL of the viewed page
+        """
+        return self.payload.get("url")
+
+    @page_url.setter
+    def page_url(self, value: str):
+        non_empty_string(value)
+        self.payload.add("url", value)
+
+    @property
+    def page_title(self) -> Optional[str]:
+        """
+        URL of the viewed page
+        """
+        return self.payload.get("page")
+
+    @page_title.setter
+    def page_title(self, value: Optional[str]):
+        self.payload.add("page", value)
+
+    @property
+    def referrer(self) -> Optional[str]:
+        """
+        The referrer of the page
+        """
+        return self.payload.get("refr")
+
+    @referrer.setter
+    def referrer(self, value: Optional[str]):
+        self.payload.add("refr", value)
+
+    @property
+    def min_x(self) -> Optional[int]:
+        """
+        Minimum page x offset seen in the last ping period
+        """
+        return self.payload.get("pp_mix")
+
+    @min_x.setter
+    def min_x(self, value: Optional[int]):
+        self.payload.add("pp_mix", value)
+
+    @property
+    def max_x(self) -> Optional[int]:
+        """
+        Maximum page x offset seen in the last ping period
+        """
+        return self.payload.get("pp_max")
+
+    @max_x.setter
+    def max_x(self, value: Optional[int]):
+        self.payload.add("pp_max", value)
+
+    @property
+    def min_y(self) -> Optional[int]:
+        """
+        Minimum page y offset seen in the last ping period
+        """
+        return self.payload.get("pp_miy")
+
+    @min_y.setter
+    def min_y(self, value: Optional[int]):
+        self.payload.add("pp_miy", value)
+
+    @property
+    def max_y(self) -> Optional[int]:
+        """
+        Maximum page y offset seen in the last ping period
+        """
+        return self.payload.get("pp_may")
+
+    @max_y.setter
+    def max_y(self, value: Optional[int]):
+        self.payload.add("pp_may", value)
diff --git a/snowplow_tracker/events/page_view.py b/snowplow_tracker/events/page_view.py
new file mode 100644
index 0000000..dc9ca51
--- /dev/null
+++ b/snowplow_tracker/events/page_view.py
@@ -0,0 +1,95 @@
+# """
+#     page_view.py
+
+#     Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+
+#     This program is licensed to you under the Apache License Version 2.0,
+#     and you may not use this file except in compliance with the Apache License
+#     Version 2.0. You may obtain a copy of the Apache License Version 2.0 at
+#     http://www.apache.org/licenses/LICENSE-2.0.
+
+#     Unless required by applicable law or agreed to in writing,
+#     software distributed under the Apache License Version 2.0 is distributed on
+#     an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+#     express or implied. See the Apache License Version 2.0 for the specific
+#     language governing permissions and limitations there under.
+# """
+from snowplow_tracker.events.event import Event
+from typing import Optional, List
+from snowplow_tracker.subject import Subject
+from snowplow_tracker.self_describing_json import SelfDescribingJson
+from snowplow_tracker.contracts import non_empty_string
+
+
+class PageView(Event):
+    """
+    Constructs a PageView event object.
+
+    When tracked, generates a "pv" or "page_view" event.
+
+    """
+
+    def __init__(
+        self,
+        page_url: str,
+        page_title: Optional[str] = None,
+        referrer: Optional[str] = None,
+        event_subject: Optional[Subject] = None,
+        context: Optional[List[SelfDescribingJson]] = None,
+        true_timestamp: Optional[float] = None,
+    ) -> None:
+        """
+        :param  page_url:       URL of the viewed page
+        :type   page_url:       non_empty_string
+        :param  page_title:     Title of the viewed page
+        :type   page_title:     string_or_none
+        :param  referrer:       Referrer of the page
+        :type   referrer:       string_or_none
+        :param  event_subject:   Optional per event subject
+        :type   event_subject:   subject | None
+        :param  context:         Custom context for the event
+        :type   context:         context_array | None
+        :param  true_timestamp:          Optional event timestamp in milliseconds
+        :type   true_timestamp:          int | float | None
+        """
+        super(PageView, self).__init__(
+            event_subject=event_subject, context=context, true_timestamp=true_timestamp
+        )
+        self.payload.add("e", "pv")
+        self.page_url = page_url
+        self.page_title = page_title
+        self.referrer = referrer
+
+    @property
+    def page_url(self) -> str:
+        """
+        URL of the viewed page
+        """
+        return self.payload.get("url")
+
+    @page_url.setter
+    def page_url(self, value: str):
+        non_empty_string(value)
+        self.payload.add("url", value)
+
+    @property
+    def page_title(self) -> Optional[str]:
+        """
+        Title of the viewed page
+        """
+        return self.payload.get("page")
+
+    @page_title.setter
+    def page_title(self, value: Optional[str]):
+        self.payload.add("page", value)
+
+    @property
+    def referrer(self) -> Optional[str]:
+        """
+        The referrer of the page
+        """
+        return self.payload.get("refr")
+
+    @referrer.setter
+    def referrer(self, value: Optional[str]):
+        self.payload.add("refr", value)
diff --git a/snowplow_tracker/events/screen_view.py b/snowplow_tracker/events/screen_view.py
new file mode 100644
index 0000000..d0cea5d
--- /dev/null
+++ b/snowplow_tracker/events/screen_view.py
@@ -0,0 +1,199 @@
+# """
+#     screen_view.py
+
+#     Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+
+#     This program is licensed to you under the Apache License Version 2.0,
+#     and you may not use this file except in compliance with the Apache License
+#     Version 2.0. You may obtain a copy of the Apache License Version 2.0 at
+#     http://www.apache.org/licenses/LICENSE-2.0.
+
+#     Unless required by applicable law or agreed to in writing,
+#     software distributed under the Apache License Version 2.0 is distributed on
+#     an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+#     express or implied. See the Apache License Version 2.0 for the specific
+#     language governing permissions and limitations there under.
+# """
+
+from typing import Optional, List
+from snowplow_tracker.typing import JsonEncoderFunction
+from snowplow_tracker.events.event import Event
+from snowplow_tracker.events.self_describing import SelfDescribing
+from snowplow_tracker import SelfDescribingJson
+from snowplow_tracker.constants import (
+    MOBILE_SCHEMA_PATH,
+    SCHEMA_TAG,
+)
+from snowplow_tracker import payload
+from snowplow_tracker.subject import Subject
+from snowplow_tracker.contracts import non_empty_string
+
+
+class ScreenView(Event):
+    """
+    Constructs a ScreenView event object.
+
+    When tracked, generates a SelfDescribing event (event type "ue").
+
+    Schema: `iglu:com.snowplowanalytics.mobile/screen_view/jsonschema/1-0-0`
+    """
+
+    def __init__(
+        self,
+        id_: str,
+        name: str,
+        type: Optional[str] = None,
+        previous_name: Optional[str] = None,
+        previous_id: Optional[str] = None,
+        previous_type: Optional[str] = None,
+        transition_type: Optional[str] = None,
+        event_subject: Optional[Subject] = None,
+        context: Optional[List[SelfDescribingJson]] = None,
+        true_timestamp: Optional[float] = None,
+    ) -> None:
+        """
+        :param  id_:            Screen view ID. This must be of type UUID.
+        :type   id_:            string
+        :param  name:           The name of the screen view event
+        :type   name:           string
+        :param  type:           The type of screen that was viewed e.g feed / carousel.
+        :type   type:           string | None
+        :param  previous_name:  The name of the previous screen.
+        :type   previous_name:  string | None
+        :param  previous_id:    The screenview ID of the previous screenview.
+        :type   previous_id:    string | None
+        :param  previous_type   The screen type of the previous screenview
+        :type   previous_type   string | None
+        :param  transition_type The type of transition that led to the screen being viewed.
+        :type   transition_type string | None
+        :param  event_subject:   Optional per event subject
+        :type   event_subject:   subject | None
+        :param  context:         Custom context for the event
+        :type   context:         context_array | None
+        :param  true_timestamp:  Optional event timestamp in milliseconds
+        :type   true_timestamp:  int | float | None
+        """
+        super(ScreenView, self).__init__(
+            event_subject=event_subject, context=context, true_timestamp=true_timestamp
+        )
+        self.screen_view_properties = {}
+        self.id_ = id_
+        self.name = name
+        self.type = type
+        self.previous_name = previous_name
+        self.previous_id = previous_id
+        self.previous_type = previous_type
+        self.transition_type = transition_type
+
+    @property
+    def id_(self) -> str:
+        """
+        Screen view ID. This must be of type UUID.
+        """
+        return self.screen_view_properties["id"]
+
+    @id_.setter
+    def id_(self, value: str):
+        non_empty_string(value)
+        self.screen_view_properties["id"] = value
+
+    @property
+    def name(self) -> str:
+        """
+        The name of the screen view event
+        """
+        return self.screen_view_properties["name"]
+
+    @name.setter
+    def name(self, value: str):
+        non_empty_string(value)
+        self.screen_view_properties["name"] = value
+
+    @property
+    def type(self) -> Optional[str]:
+        """
+        The type of screen that was viewed e.g feed / carousel
+        """
+        return self.screen_view_properties["type"]
+
+    @type.setter
+    def type(self, value: Optional[str]):
+        if value is not None:
+            self.screen_view_properties["type"] = value
+
+    @property
+    def previous_name(self) -> Optional[str]:
+        """
+        The name of the previous screen.
+        """
+        return self.screen_view_properties["previousName"]
+
+    @previous_name.setter
+    def previous_name(self, value: Optional[str]):
+        if value is not None:
+            self.screen_view_properties["previousName"] = value
+
+    @property
+    def previous_id(self) -> Optional[str]:
+        """
+        The screenview ID of the previous screenview.
+        """
+        return self.screen_view_properties["previousId"]
+
+    @previous_id.setter
+    def previous_id(self, value: Optional[str]):
+        if value is not None:
+            self.screen_view_properties["previousId"] = value
+
+    @property
+    def previous_type(self) -> Optional[str]:
+        """
+        The screen type of the previous screenview
+        """
+        return self.screen_view_properties["previousType"]
+
+    @previous_type.setter
+    def previous_type(self, value: Optional[str]):
+        if value is not None:
+            self.screen_view_properties["previousType"] = value
+
+    @property
+    def transition_type(self) -> Optional[str]:
+        """
+        The type of transition that led to the screen being viewed
+        """
+        return self.screen_view_properties["transitionType"]
+
+    @transition_type.setter
+    def transition_type(self, value: Optional[str]):
+        if value is not None:
+            self.screen_view_properties["transitionType"] = value
+
+    def build_payload(
+        self,
+        encode_base64: bool,
+        json_encoder: Optional[JsonEncoderFunction],
+        subject: Optional[Subject] = None,
+    ) -> "payload.Payload":
+        """
+        :param encode_base64:    Whether JSONs in the payload should be base-64 encoded
+        :type  encode_base64:    bool
+        :param json_encoder:     Custom JSON serializer that gets called on non-serializable object
+        :type  json_encoder:     function | None
+        :param  subject:         Optional per event subject
+        :type   subject:         subject | None
+        :rtype:                  payload.Payload
+        """
+        event_json = SelfDescribingJson(
+            "%s/screen_view/%s/1-0-0" % (MOBILE_SCHEMA_PATH, SCHEMA_TAG),
+            self.screen_view_properties,
+        )
+        self_describing = SelfDescribing(
+            event_json=event_json,
+            event_subject=self.event_subject,
+            context=self.context,
+            true_timestamp=self.true_timestamp,
+        )
+        return self_describing.build_payload(
+            encode_base64, json_encoder, subject=subject
+        )
diff --git a/snowplow_tracker/events/self_describing.py b/snowplow_tracker/events/self_describing.py
new file mode 100644
index 0000000..e560eb7
--- /dev/null
+++ b/snowplow_tracker/events/self_describing.py
@@ -0,0 +1,98 @@
+# """
+#     self_describing.py
+
+#     Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+
+#     This program is licensed to you under the Apache License Version 2.0,
+#     and you may not use this file except in compliance with the Apache License
+#     Version 2.0. You may obtain a copy of the Apache License Version 2.0 at
+#     http://www.apache.org/licenses/LICENSE-2.0.
+
+#     Unless required by applicable law or agreed to in writing,
+#     software distributed under the Apache License Version 2.0 is distributed on
+#     an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+#     express or implied. See the Apache License Version 2.0 for the specific
+#     language governing permissions and limitations there under.
+# """
+from typing import Optional, List
+from snowplow_tracker.typing import JsonEncoderFunction
+from snowplow_tracker.events.event import Event
+from snowplow_tracker import SelfDescribingJson
+from snowplow_tracker.constants import UNSTRUCT_EVENT_SCHEMA
+from snowplow_tracker import payload
+from snowplow_tracker.subject import Subject
+from snowplow_tracker.contracts import non_empty
+
+
+class SelfDescribing(Event):
+    """
+    Constructs a SelfDescribing event object.
+
+    This is a customisable event type which allows you to track anything describable
+    by a JsonSchema.
+
+    When tracked, generates a self-describing event (event type "ue").
+    """
+
+    def __init__(
+        self,
+        event_json: SelfDescribingJson,
+        event_subject: Optional[Subject] = None,
+        context: Optional[List[SelfDescribingJson]] = None,
+        true_timestamp: Optional[float] = None,
+    ) -> None:
+        """
+        :param  event_json:      The properties of the event. Has two field:
+                                 A "data" field containing the event properties and
+                                 A "schema" field identifying the schema against which the data is validated
+        :type   event_json:      self_describing_json
+        :param  event_subject:   Optional per event subject
+        :type   event_subject:   subject | None
+        :param  context:         Custom context for the event
+        :type   context:         context_array | None
+        :param  true_timestamp:          Optional event timestamp in milliseconds
+        :type   true_timestamp:          int | float | None
+        """
+        super(SelfDescribing, self).__init__(
+            event_subject=event_subject, context=context, true_timestamp=true_timestamp
+        )
+        self.payload.add("e", "ue")
+        self.event_json = event_json
+
+    @property
+    def event_json(self) -> SelfDescribingJson:
+        """
+        The properties of the event. Has two field:
+            A "data" field containing the event properties and
+            A "schema" field identifying the schema against which the data is validated
+        """
+        return self._event_json
+
+    @event_json.setter
+    def event_json(self, value: SelfDescribingJson):
+        self._event_json = value
+
+    def build_payload(
+        self,
+        encode_base64: bool,
+        json_encoder: Optional[JsonEncoderFunction],
+        subject: Optional[Subject] = None,
+    ) -> "payload.Payload":
+        """
+        :param encode_base64:    Whether JSONs in the payload should be base-64 encoded
+        :type  encode_base64:    bool
+        :param json_encoder:     Custom JSON serializer that gets called on non-serializable object
+        :type  json_encoder:     function | None
+        :param  subject:         Optional per event subject
+        :type   subject:         subject | None
+        :rtype:                  payload.Payload
+        """
+
+        envelope = SelfDescribingJson(
+            UNSTRUCT_EVENT_SCHEMA, self.event_json.to_json()
+        ).to_json()
+        self.payload.add_json(envelope, encode_base64, "ue_px", "ue_pr", json_encoder)
+
+        return super(SelfDescribing, self).build_payload(
+            encode_base64=encode_base64, json_encoder=json_encoder, subject=subject
+        )
diff --git a/snowplow_tracker/events/structured_event.py b/snowplow_tracker/events/structured_event.py
new file mode 100644
index 0000000..a92acc0
--- /dev/null
+++ b/snowplow_tracker/events/structured_event.py
@@ -0,0 +1,134 @@
+# """
+#     struct_event.py
+
+#     Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+
+#     This program is licensed to you under the Apache License Version 2.0,
+#     and you may not use this file except in compliance with the Apache License
+#     Version 2.0. You may obtain a copy of the Apache License Version 2.0 at
+#     http://www.apache.org/licenses/LICENSE-2.0.
+
+#     Unless required by applicable law or agreed to in writing,
+#     software distributed under the Apache License Version 2.0 is distributed on
+#     an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+#     express or implied. See the Apache License Version 2.0 for the specific
+#     language governing permissions and limitations there under.
+# """
+from snowplow_tracker.events.event import Event
+from typing import Optional, List
+from snowplow_tracker.subject import Subject
+from snowplow_tracker.self_describing_json import SelfDescribingJson
+from snowplow_tracker.contracts import non_empty_string
+
+
+class StructuredEvent(Event):
+    """
+    Constructs a Structured event object.
+
+    This event type is provided to be roughly equivalent to Google Analytics-style events.
+    Note that it is not automatically clear what data should be placed in what field.
+    To aid data quality and modeling, agree on business-wide definitions when designing
+    your tracking strategy.
+
+    We recommend using SelfDescribing - fully custom - events instead.
+
+    When tracked, generates a "struct" or "se" event.
+    """
+
+    def __init__(
+        self,
+        category: str,
+        action: str,
+        label: Optional[str] = None,
+        property_: Optional[str] = None,
+        value: Optional[int] = None,
+        event_subject: Optional[Subject] = None,
+        context: Optional[List[SelfDescribingJson]] = None,
+        true_timestamp: Optional[float] = None,
+    ) -> None:
+        """
+        :param  category:       Category of the event
+        :type   category:       non_empty_string
+        :param  action:         The event itself
+        :type   action:         non_empty_string
+        :param  label:          Refer to the object the action is
+                                performed on
+        :type   label:          string_or_none
+        :param  property_:      Property associated with either the action
+                                or the object
+        :type   property_:      string_or_none
+        :param  value:          A value associated with the user action
+        :type   value:          int | float | None
+        :param  event_subject:   Optional per event subject
+        :type   event_subject:   subject | None
+        :param  context:         Custom context for the event
+        :type   context:         context_array | None
+        :param  true_timestamp:          Optional event timestamp in milliseconds
+        :type   true_timestamp:          int | float | None
+        """
+        super(StructuredEvent, self).__init__(
+            event_subject=event_subject, context=context, true_timestamp=true_timestamp
+        )
+        self.payload.add("e", "se")
+        self.category = category
+        self.action = action
+        self.label = label
+        self.property_ = property_
+        self.value = value
+
+    @property
+    def category(self) -> Optional[str]:
+        """
+        Category of the event
+        """
+        return self.payload.get("se_ca")
+
+    @category.setter
+    def category(self, value: Optional[str]):
+        non_empty_string(value)
+        self.payload.add("se_ca", value)
+
+    @property
+    def action(self) -> Optional[str]:
+        """
+        The event itself
+        """
+        return self.payload.get("se_ac")
+
+    @action.setter
+    def action(self, value: Optional[str]):
+        non_empty_string(value)
+        self.payload.add("se_ac", value)
+
+    @property
+    def label(self) -> Optional[str]:
+        """
+        Refer to the object the action is performed on
+        """
+        return self.payload.get("se_la")
+
+    @label.setter
+    def label(self, value: Optional[str]):
+        self.payload.add("se_la", value)
+
+    @property
+    def property_(self) -> Optional[str]:
+        """
+        Property associated with either the action or the object
+        """
+        return self.payload.get("se_pr")
+
+    @property_.setter
+    def property_(self, value: Optional[str]):
+        self.payload.add("se_pr", value)
+
+    @property
+    def value(self) -> Optional[int]:
+        """
+        A value associated with the user action
+        """
+        return self.payload.get("se_va")
+
+    @value.setter
+    def value(self, value: Optional[int]):
+        self.payload.add("se_va", value)
diff --git a/snowplow_tracker/self_describing_json.py b/snowplow_tracker/self_describing_json.py
index b386c03..8f7b65e 100644
--- a/snowplow_tracker/self_describing_json.py
+++ b/snowplow_tracker/self_describing_json.py
@@ -19,6 +19,7 @@ import json
 from typing import Union
 
 from snowplow_tracker.typing import PayloadDict, PayloadDictList
+from snowplow_tracker.contracts import non_empty_string
 
 
 class SelfDescribingJson(object):
@@ -26,6 +27,15 @@ class SelfDescribingJson(object):
         self.schema = schema
         self.data = data
 
+    @property
+    def schema(self) -> str:
+        return self._schema
+
+    @schema.setter
+    def schema(self, value: str):
+        non_empty_string(value)
+        self._schema = value
+
     def to_json(self) -> PayloadDict:
         return {"schema": self.schema, "data": self.data}
 
diff --git a/snowplow_tracker/tracker.py b/snowplow_tracker/tracker.py
index 6831508..f0e92fd 100644
--- a/snowplow_tracker/tracker.py
+++ b/snowplow_tracker/tracker.py
@@ -20,9 +20,24 @@ import uuid
 from typing import Any, Optional, Union, List, Dict, Sequence
 from warnings import warn
 
-from snowplow_tracker import payload, _version, SelfDescribingJson
-from snowplow_tracker import subject as _subject
+from snowplow_tracker import payload, SelfDescribingJson
+from snowplow_tracker.subject import Subject
 from snowplow_tracker.contracts import non_empty_string, one_of, non_empty, form_element
+from snowplow_tracker.constants import (
+    VERSION,
+    DEFAULT_ENCODE_BASE64,
+    BASE_SCHEMA_PATH,
+    SCHEMA_TAG,
+)
+
+from snowplow_tracker.events import (
+    Event,
+    PagePing,
+    PageView,
+    SelfDescribing,
+    StructuredEvent,
+    ScreenView,
+)
 from snowplow_tracker.typing import (
     JsonEncoderFunction,
     EmitterProtocol,
@@ -33,19 +48,6 @@ from snowplow_tracker.typing import (
     FormClasses,
 )
 
-"""
-Constants & config
-"""
-
-VERSION = "py-%s" % _version.__version__
-DEFAULT_ENCODE_BASE64 = True
-BASE_SCHEMA_PATH = "iglu:com.snowplowanalytics.snowplow"
-MOBILE_SCHEMA_PATH = "iglu:com.snowplowanalytics.mobile"
-SCHEMA_TAG = "jsonschema"
-CONTEXT_SCHEMA = "%s/contexts/%s/1-0-1" % (BASE_SCHEMA_PATH, SCHEMA_TAG)
-UNSTRUCT_EVENT_SCHEMA = "%s/unstruct_event/%s/1-0-0" % (BASE_SCHEMA_PATH, SCHEMA_TAG)
-ContextArray = List[SelfDescribingJson]
-
 """
 Tracker class
 """
@@ -56,7 +58,7 @@ class Tracker:
         self,
         namespace: str,
         emitters: Union[List[EmitterProtocol], EmitterProtocol],
-        subject: Optional[_subject.Subject] = None,
+        subject: Optional[Subject] = None,
         app_id: Optional[str] = None,
         encode_base64: bool = DEFAULT_ENCODE_BASE64,
         json_encoder: Optional[JsonEncoderFunction] = None,
@@ -76,7 +78,7 @@ class Tracker:
         :type  json_encoder:     function | None
         """
         if subject is None:
-            subject = _subject.Subject()
+            subject = Subject()
 
         if type(emitters) is list:
             non_empty(emitters)
@@ -122,62 +124,42 @@ class Tracker:
     Tracking methods
     """
 
-    def track(self, pb: payload.Payload) -> Optional[str]:
-        """
-        Send the payload to a emitter. Returns the tracked event ID.
-
-        :param  pb:              Payload builder
-        :type   pb:              payload
-        :rtype:                  String
-        """
-        for emitter in self.emitters:
-            emitter.input(pb.nv_pairs)
-
-        if "eid" in pb.nv_pairs.keys():
-            return pb.nv_pairs["eid"]
-
-    def complete_payload(
+    def track(
         self,
-        pb: payload.Payload,
-        context: Optional[List[SelfDescribingJson]],
-        tstamp: Optional[float],
-        event_subject: Optional[_subject.Subject],
+        event: Event,
     ) -> Optional[str]:
         """
-        Called by all tracking events to add the standard name-value pairs
-        to the Payload object irrespective of the tracked event.
-
-        :param  pb:              Payload builder
-        :type   pb:              payload
-        :param  context:         Custom context for the event
-        :type   context:         context_array | None
-        :param  tstamp:          Optional event timestamp in milliseconds
-        :type   tstamp:          int | float | None
-        :param  event_subject:   Optional per event subject
-        :type   event_subject:   subject | None
+        Send the event payload to a emitter. Returns the tracked event ID.
+        :param  event:           Event
+        :type   event:           events.Event
         :rtype:                  String
         """
-        pb.add("eid", Tracker.get_uuid())
 
-        pb.add("dtm", Tracker.get_timestamp())
-        if tstamp is not None:
-            pb.add("ttm", Tracker.get_timestamp(tstamp))
+        payload = self.complete_payload(
+            event=event,
+        )
 
-        if context is not None:
-            context_jsons = list(map(lambda c: c.to_json(), context))
-            context_envelope = SelfDescribingJson(
-                CONTEXT_SCHEMA, context_jsons
-            ).to_json()
-            pb.add_json(
-                context_envelope, self.encode_base64, "cx", "co", self.json_encoder
-            )
+        for emitter in self.emitters:
+            emitter.input(payload.nv_pairs)
 
-        pb.add_dict(self.standard_nv_pairs)
+        if "eid" in payload.nv_pairs.keys():
+            return payload.nv_pairs["eid"]
 
-        fin_subject = event_subject if event_subject is not None else self.subject
-        pb.add_dict(fin_subject.standard_nv_pairs)
+    def complete_payload(
+        self,
+        event: Event,
+    ) -> payload.Payload:
+        payload = event.build_payload(
+            encode_base64=self.encode_base64,
+            json_encoder=self.json_encoder,
+            subject=self.subject,
+        )
+
+        payload.add("eid", Tracker.get_uuid())
+        payload.add("dtm", Tracker.get_timestamp())
+        payload.add_dict(self.standard_nv_pairs)
 
-        return self.track(pb)
+        return payload
 
     def track_page_view(
         self,
@@ -186,7 +168,7 @@ class Tracker:
         referrer: Optional[str] = None,
         context: Optional[List[SelfDescribingJson]] = None,
         tstamp: Optional[float] = None,
-        event_subject: Optional[_subject.Subject] = None,
+        event_subject: Optional[Subject] = None,
     ) -> "Tracker":
         """
         :param  page_url:       URL of the viewed page
@@ -203,15 +185,22 @@ class Tracker:
         :type   event_subject:  subject | None
         :rtype:                 Tracker
         """
-        non_empty_string(page_url)
+        warn(
+            "track_page_view will be removed in future versions. Please use the new PageView class to track the event.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
 
-        pb = payload.Payload()
-        pb.add("e", "pv")  # pv: page view
-        pb.add("url", page_url)
-        pb.add("page", page_title)
-        pb.add("refr", referrer)
+        pv = PageView(
+            page_url=page_url,
+            page_title=page_title,
+            referrer=referrer,
+            event_subject=event_subject,
+            context=context,
+            true_timestamp=tstamp,
+        )
 
-        self.complete_payload(pb, context, tstamp, event_subject)
+        self.track(event=pv)
         return self
 
     def track_page_ping(
@@ -225,7 +214,7 @@ class Tracker:
         max_y: Optional[int] = None,
         context: Optional[List[SelfDescribingJson]] = None,
         tstamp: Optional[float] = None,
-        event_subject: Optional[_subject.Subject] = None,
+        event_subject: Optional[Subject] = None,
     ) -> "Tracker":
         """
         :param  page_url:       URL of the viewed page
@@ -250,19 +239,26 @@ class Tracker:
         :type   event_subject:  subject | None
         :rtype:                 Tracker
         """
-        non_empty_string(page_url)
+        warn(
+            "track_page_ping will be removed in future versions. Please use the new PagePing class to track the event.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
 
-        pb = payload.Payload()
-        pb.add("e", "pp")  # pp: page ping
-        pb.add("url", page_url)
-        pb.add("page", page_title)
-        pb.add("refr", referrer)
-        pb.add("pp_mix", min_x)
-        pb.add("pp_max", max_x)
-        pb.add("pp_miy", min_y)
-        pb.add("pp_may", max_y)
+        pp = PagePing(
+            page_url=page_url,
+            page_title=page_title,
+            referrer=referrer,
+            min_x=min_x,
+            max_x=max_x,
+            min_y=min_y,
+            max_y=max_y,
+            context=context,
+            true_timestamp=tstamp,
+            event_subject=event_subject,
+        )
 
-        self.complete_payload(pb, context, tstamp, event_subject)
+        self.track(event=pp)
         return self
 
     def track_link_click(
@@ -274,27 +270,32 @@ class Tracker:
         element_content: Optional[str] = None,
         context: Optional[List[SelfDescribingJson]] = None,
         tstamp: Optional[float] = None,
-        event_subject: Optional[_subject.Subject] = None,
+        event_subject: Optional[Subject] = None,
     ) -> "Tracker":
         """
-        :param  target_url:     Target URL of the link
-        :type   target_url:     non_empty_string
-        :param  element_id:     ID attribute of the HTML element
-        :type   element_id:     string_or_none
+        :param  target_url:         Target URL of the link
+        :type   target_url:         non_empty_string
+        :param  element_id:         ID attribute of the HTML element
+        :type   element_id:         string_or_none
         :param  element_classes:    Classes of the HTML element
         :type   element_classes:    list(str) | tuple(str,\\*) | None
         :param  element_target:     ID attribute of the HTML element
         :type   element_target:     string_or_none
         :param  element_content:    The content of the HTML element
         :type   element_content:    string_or_none
-        :param  context:        Custom context for the event
-        :type   context:        context_array | None
-        :param  tstamp:         Optional event timestamp in milliseconds
-        :type   tstamp:         int | float | None
-        :param  event_subject:  Optional per event subject
-        :type   event_subject:  subject | None
-        :rtype:                 Tracker
+        :param  context:            Custom context for the event
+        :type   context:            context_array | None
+        :param  tstamp:             Optional event timestamp in milliseconds
+        :type   tstamp:             int | float | None
+        :param  event_subject:      Optional per event subject
+        :type   event_subject:      subject | None
+        :rtype:                     Tracker
         """
+        warn(
+            "track_link_click will be removed in future versions. Please use the new SelfDescribing class to track the event.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
         non_empty_string(target_url)
 
         properties = {}
@@ -312,7 +313,12 @@ class Tracker:
             "%s/link_click/%s/1-0-1" % (BASE_SCHEMA_PATH, SCHEMA_TAG), properties
         )
 
-        self.track_self_describing_event(event_json, context, tstamp, event_subject)
+        self.track_self_describing_event(
+            event_json=event_json,
+            context=context,
+            true_timestamp=tstamp,
+            event_subject=event_subject,
+        )
         return self
 
     def track_add_to_cart(
@@ -325,7 +331,7 @@ class Tracker:
         currency: Optional[str] = None,
         context: Optional[List[SelfDescribingJson]] = None,
         tstamp: Optional[float] = None,
-        event_subject: Optional[_subject.Subject] = None,
+        event_subject: Optional[Subject] = None,
     ) -> "Tracker":
         """
         :param  sku:            Item SKU or ID
@@ -371,7 +377,12 @@ class Tracker:
             "%s/add_to_cart/%s/1-0-0" % (BASE_SCHEMA_PATH, SCHEMA_TAG), properties
         )
 
-        self.track_self_describing_event(event_json, context, tstamp, event_subject)
+        self.track_self_describing_event(
+            event_json=event_json,
+            context=context,
+            true_timestamp=tstamp,
+            event_subject=event_subject,
+        )
         return self
 
     def track_remove_from_cart(
@@ -384,7 +395,7 @@ class Tracker:
         currency: Optional[str] = None,
         context: Optional[List[SelfDescribingJson]] = None,
         tstamp: Optional[float] = None,
-        event_subject: Optional[_subject.Subject] = None,
+        event_subject: Optional[Subject] = None,
     ) -> "Tracker":
         """
         :param  sku:            Item SKU or ID
@@ -430,7 +441,12 @@ class Tracker:
             "%s/remove_from_cart/%s/1-0-0" % (BASE_SCHEMA_PATH, SCHEMA_TAG), properties
         )
 
-        self.track_self_describing_event(event_json, context, tstamp, event_subject)
+        self.track_self_describing_event(
+            event_json=event_json,
+            context=context,
+            true_timestamp=tstamp,
+            event_subject=event_subject,
+        )
         return self
 
     def track_form_change(
@@ -443,29 +459,35 @@ class Tracker:
         element_classes: Optional[ElementClasses] = None,
         context: Optional[List[SelfDescribingJson]] = None,
         tstamp: Optional[float] = None,
-        event_subject: Optional[_subject.Subject] = None,
+        event_subject: Optional[Subject] = None,
     ) -> "Tracker":
         """
-        :param  form_id:        ID attribute of the HTML form
-        :type   form_id:        non_empty_string
-        :param  element_id:     ID attribute of the HTML element
-        :type   element_id:     string_or_none
-        :param  node_name:      Type of input element
-        :type   node_name:      form_node_name
-        :param  value:          Value of the input element
-        :type   value:          string_or_none
-        :param  type_:          Type of data the element represents
-        :type   type_:          non_empty_string, form_type
+        :param  form_id:            ID attribute of the HTML form
+        :type   form_id:            non_empty_string
+        :param  element_id:         ID attribute of the HTML element
+        :type   element_id:         string_or_none
+        :param  node_name:          Type of input element
+        :type   node_name:          form_node_name
+        :param  value:              Value of the input element
+        :type   value:              string_or_none
+        :param  type_:              Type of data the element represents
+        :type   type_:              non_empty_string, form_type
         :param  element_classes:    Classes of the HTML element
         :type   element_classes:    list(str) | tuple(str,\\*) | None
-        :param  context:        Custom context for the event
-        :type   context:        context_array | None
-        :param  tstamp:         Optional event timestamp in milliseconds
-        :type   tstamp:         int | float | None
-        :param  event_subject:  Optional per event subject
-        :type   event_subject:  subject | None
-        :rtype:                 Tracker
+        :param  context:            Custom context for the event
+        :type   context:            context_array | None
+        :param  tstamp:             Optional event timestamp in milliseconds
+        :type   tstamp:             int | float | None
+        :param  event_subject:      Optional per event subject
+        :type   event_subject:      subject | None
+        :rtype:                     Tracker
         """
+        warn(
+            "track_form_change will be removed in future versions. Please use the new SelfDescribing class to track the event.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+
         non_empty_string(form_id)
         one_of(node_name, FORM_NODE_NAMES)
         if type_ is not None:
@@ -485,7 +507,12 @@ class Tracker:
             "%s/change_form/%s/1-0-0" % (BASE_SCHEMA_PATH, SCHEMA_TAG), properties
         )
 
-        self.track_self_describing_event(event_json, context, tstamp, event_subject)
+        self.track_self_describing_event(
+            event_json=event_json,
+            context=context,
+            true_timestamp=tstamp,
+            event_subject=event_subject,
+        )
         return self
 
     def track_form_submit(
@@ -495,7 +522,7 @@ class Tracker:
         elements: Optional[List[Dict[str, Any]]] = None,
         context: Optional[List[SelfDescribingJson]] = None,
         tstamp: Optional[float] = None,
-        event_subject: Optional[_subject.Subject] = None,
+        event_subject: Optional[Subject] = None,
     ) -> "Tracker":
         """
         :param  form_id:        ID attribute of the HTML form
@@ -512,7 +539,13 @@ class Tracker:
         :type   event_subject:  subject | None
         :rtype:                 Tracker
         """
+        warn(
+            "track_form_submit will be removed in future versions. Please use the new SelfDescribing class to track the event.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
         non_empty_string(form_id)
+
         for element in elements or []:
             form_element(element)
 
@@ -527,7 +560,12 @@ class Tracker:
             "%s/submit_form/%s/1-0-0" % (BASE_SCHEMA_PATH, SCHEMA_TAG), properties
         )
 
-        self.track_self_describing_event(event_json, context, tstamp, event_subject)
+        self.track_self_describing_event(
+            event_json=event_json,
+            context=context,
+            true_timestamp=tstamp,
+            event_subject=event_subject,
+        )
         return self
 
     def track_site_search(
@@ -538,7 +576,7 @@ class Tracker:
         page_results: Optional[int] = None,
         context: Optional[List[SelfDescribingJson]] = None,
         tstamp: Optional[float] = None,
-        event_subject: Optional[_subject.Subject] = None,
+        event_subject: Optional[Subject] = None,
     ) -> "Tracker":
         """
         :param  terms:          Search terms
@@ -557,6 +595,11 @@ class Tracker:
         :type   event_subject:  subject | None
         :rtype:                 Tracker
         """
+        warn(
+            "track_site_search will be removed in future versions. Please use the new SelfDescribing class to track the event.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
         non_empty(terms)
 
         properties = {}
@@ -572,7 +615,12 @@ class Tracker:
             "%s/site_search/%s/1-0-0" % (BASE_SCHEMA_PATH, SCHEMA_TAG), properties
         )
 
-        self.track_self_describing_event(event_json, context, tstamp, event_subject)
+        self.track_self_describing_event(
+            event_json=event_json,
+            context=context,
+            true_timestamp=tstamp,
+            event_subject=event_subject,
+        )
         return self
 
     def track_ecommerce_transaction_item(
@@ -586,33 +634,33 @@ class Tracker:
         currency: Optional[str] = None,
         context: Optional[List[SelfDescribingJson]] = None,
         tstamp: Optional[float] = None,
-        event_subject: Optional[_subject.Subject] = None,
+        event_subject: Optional[Subject] = None,
     ) -> "Tracker":
         """
         This is an internal method called by track_ecommerce_transaction.
         It is not for public use.
 
-        :param  order_id:    Order ID
-        :type   order_id:    non_empty_string
-        :param  sku:         Item SKU
-        :type   sku:         non_empty_string
-        :param  price:       Item price
-        :type   price:       int | float
-        :param  quantity:    Item quantity
-        :type   quantity:    int
-        :param  name:        Item name
-        :type   name:        string_or_none
-        :param  category:    Item category
-        :type   category:    string_or_none
-        :param  currency:    The currency the price is expressed in
-        :type   currency:    string_or_none
-        :param  context:     Custom context for the event
-        :type   context:     context_array | None
-        :param  tstamp:      Optional event timestamp in milliseconds
-        :type   tstamp:      int | float | None
+        :param  order_id:       Order ID
+        :type   order_id:       non_empty_string
+        :param  sku:            Item SKU
+        :type   sku:            non_empty_string
+        :param  price:          Item price
+        :type   price:          int | float
+        :param  quantity:       Item quantity
+        :type   quantity:       int
+        :param  name:           Item name
+        :type   name:           string_or_none
+        :param  category:       Item category
+        :type   category:       string_or_none
+        :param  currency:       The currency the price is expressed in
+        :type   currency:       string_or_none
+        :param  context:        Custom context for the event
+        :type   context:        context_array | None
+        :param  tstamp:         Optional event timestamp in milliseconds
+        :type   tstamp:         int | float | None
         :param  event_subject:  Optional per event subject
         :type   event_subject:  subject | None
-        :rtype:              Tracker
+        :rtype:                 Tracker
         """
         warn(
             "track_ecommerce_transaction_item will be deprecated in future versions.",
@@ -622,17 +670,19 @@ class Tracker:
         non_empty_string(order_id)
         non_empty_string(sku)
 
-        pb = payload.Payload()
-        pb.add("e", "ti")
-        pb.add("ti_id", order_id)
-        pb.add("ti_sk", sku)
-        pb.add("ti_nm", name)
-        pb.add("ti_ca", category)
-        pb.add("ti_pr", price)
-        pb.add("ti_qu", quantity)
-        pb.add("ti_cu", currency)
-
-        self.complete_payload(pb, context, tstamp, event_subject)
+        event = Event(
+            event_subject=event_subject, context=context, true_timestamp=tstamp
+        )
+        event.payload.add("e", "ti")
+        event.payload.add("ti_id", order_id)
+        event.payload.add("ti_sk", sku)
+        event.payload.add("ti_nm", name)
+        event.payload.add("ti_ca", category)
+        event.payload.add("ti_pr", price)
+        event.payload.add("ti_qu", quantity)
+        event.payload.add("ti_cu", currency)
+
+        self.track(event=event)
         return self
 
     def track_ecommerce_transaction(
@@ -649,7 +699,7 @@ class Tracker:
         items: Optional[List[Dict[str, Any]]] = None,
         context: Optional[List[SelfDescribingJson]] = None,
         tstamp: Optional[float] = None,
-        event_subject: Optional[_subject.Subject] = None,
+        event_subject: Optional[Subject] = None,
     ) -> "Tracker":
         """
         :param  order_id:       ID of the eCommerce transaction
@@ -687,29 +737,32 @@ class Tracker:
         )
         non_empty_string(order_id)
 
-        pb = payload.Payload()
-        pb.add("e", "tr")
-        pb.add("tr_id", order_id)
-        pb.add("tr_tt", total_value)
-        pb.add("tr_af", affiliation)
-        pb.add("tr_tx", tax_value)
-        pb.add("tr_sh", shipping)
-        pb.add("tr_ci", city)
-        pb.add("tr_st", state)
-        pb.add("tr_co", country)
-        pb.add("tr_cu", currency)
+        event = Event(
+            event_subject=event_subject, context=context, true_timestamp=tstamp
+        )
+        event.payload.add("e", "tr")
+        event.payload.add("tr_id", order_id)
+        event.payload.add("tr_tt", total_value)
+        event.payload.add("tr_af", affiliation)
+        event.payload.add("tr_tx", tax_value)
+        event.payload.add("tr_sh", shipping)
+        event.payload.add("tr_ci", city)
+        event.payload.add("tr_st", state)
+        event.payload.add("tr_co", country)
+        event.payload.add("tr_cu", currency)
 
         tstamp = Tracker.get_timestamp(tstamp)
 
-        self.complete_payload(pb, context, tstamp, event_subject)
+        self.track(event=event)
 
         if items is None:
             items = []
         for item in items:
-            item["tstamp"] = tstamp
-            item["event_subject"] = event_subject
             item["order_id"] = order_id
             item["currency"] = currency
+            item["tstamp"] = tstamp
+            item["event_subject"] = event_subject
+            item["context"] = context
             self.track_ecommerce_transaction_item(**item)
 
         return self
@@ -720,7 +773,7 @@ class Tracker:
         id_: Optional[str] = None,
         context: Optional[List[SelfDescribingJson]] = None,
         tstamp: Optional[float] = None,
-        event_subject: Optional[_subject.Subject] = None,
+        event_subject: Optional[Subject] = None,
     ) -> "Tracker":
         """
         :param  name:           The name of the screen view event
@@ -736,7 +789,7 @@ class Tracker:
         :rtype:                 Tracker
         """
         warn(
-            "track_screen_view will be deprecated in future versions. Please use track_mobile_screen_view.",
+            "track_screen_view will be removed in future versions. Please use the new ScreenView class to track the event.",
             DeprecationWarning,
             stacklevel=2,
         )
@@ -751,13 +804,18 @@ class Tracker:
             screen_view_properties,
         )
 
-        self.track_self_describing_event(event_json, context, tstamp, event_subject)
+        self.track_self_describing_event(
+            event_json=event_json,
+            context=context,
+            true_timestamp=tstamp,
+            event_subject=event_subject,
+        )
         return self
 
     def track_mobile_screen_view(
         self,
+        name: str,
         id_: Optional[str] = None,
-        name: Optional[str] = None,
         type: Optional[str] = None,
         previous_name: Optional[str] = None,
         previous_id: Optional[str] = None,
@@ -765,13 +823,13 @@ class Tracker:
         transition_type: Optional[str] = None,
         context: Optional[List[SelfDescribingJson]] = None,
         tstamp: Optional[float] = None,
-        event_subject: Optional[_subject.Subject] = None,
+        event_subject: Optional[Subject] = None,
     ) -> "Tracker":
         """
-        :param  id_:            Screen view ID. This must be of type UUID.
-        :type   id_:            string | None
         :param  name:           The name of the screen view event
         :type   name:           string_or_none
+        :param  id_:            Screen view ID. This must be of type UUID.
+        :type   id_:            string | None
         :param  type:           The type of screen that was viewed e.g feed / carousel.
         :type   type:           string | None
         :param  previous_name:  The name of the previous screen.
@@ -790,31 +848,28 @@ class Tracker:
         :type   event_subject:  subject | None
         :rtype:                 Tracker
         """
-        screen_view_properties = {}
-
+        warn(
+            "track_mobile_screen_view will be removed in future versions. Please use the new ScreenView class to track the event.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
         if id_ is None:
             id_ = self.get_uuid()
 
-        screen_view_properties["id"] = id_
-
-        if name is not None:
-            screen_view_properties["name"] = name
-        if type is not None:
-            screen_view_properties["type"] = type
-        if previous_name is not None:
-            screen_view_properties["previousName"] = previous_name
-        if previous_id is not None:
-            screen_view_properties["previousId"] = previous_id
-        if previous_type is not None:
-            screen_view_properties["previousType"] = previous_type
-        if transition_type is not None:
-            screen_view_properties["transitionType"] = transition_type
-
-        event_json = SelfDescribingJson(
-            "%s/screen_view/%s/1-0-0" % (MOBILE_SCHEMA_PATH, SCHEMA_TAG),
-            screen_view_properties,
+        sv = ScreenView(
+            name=name,
+            id_=id_,
+            type=type,
+            previous_name=previous_name,
+            previous_id=previous_id,
+            previous_type=previous_type,
+            transition_type=transition_type,
+            event_subject=event_subject,
+            context=context,
+            true_timestamp=tstamp,
         )
-        self.track_self_describing_event(event_json, context, tstamp, event_subject)
+
+        self.track(event=sv)
         return self
 
     def track_struct_event(
@@ -826,7 +881,7 @@ class Tracker:
         value: Optional[float] = None,
         context: Optional[List[SelfDescribingJson]] = None,
         tstamp: Optional[float] = None,
-        event_subject: Optional[_subject.Subject] = None,
+        event_subject: Optional[Subject] = None,
     ) -> "Tracker":
         """
         :param  category:       Category of the event
@@ -849,18 +904,25 @@ class Tracker:
         :type   event_subject:  subject | None
         :rtype:                 Tracker
         """
-        non_empty_string(category)
-        non_empty_string(action)
-
-        pb = payload.Payload()
-        pb.add("e", "se")
-        pb.add("se_ca", category)
-        pb.add("se_ac", action)
-        pb.add("se_la", label)
-        pb.add("se_pr", property_)
-        pb.add("se_va", value)
+        warn(
+            "track_struct_event will be removed in future versions. Please use the new Structured class to track the event.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        se = StructuredEvent(
+            category=category,
+            action=action,
+            label=label,
+            property_=property_,
+            value=value,
+            context=context,
+            true_timestamp=tstamp,
+            event_subject=event_subject,
+        )
 
-        self.complete_payload(pb, context, tstamp, event_subject)
+        self.track(
+            event=se,
+        )
         return self
 
     def track_self_describing_event(
@@ -868,7 +930,7 @@ class Tracker:
         event_json: SelfDescribingJson,
         context: Optional[List[SelfDescribingJson]] = None,
         tstamp: Optional[float] = None,
-        event_subject: Optional[_subject.Subject] = None,
+        event_subject: Optional[Subject] = None,
     ) -> "Tracker":
         """
         :param  event_json:      The properties of the event. Has two field:
@@ -883,17 +945,21 @@ class Tracker:
         :type   event_subject:   subject | None
         :rtype:                  Tracker
         """
+        warn(
+            "track_self_describing_event will be removed in future versions. Please use the new SelfDescribing class to track the event.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
 
-        envelope = SelfDescribingJson(
-            UNSTRUCT_EVENT_SCHEMA, event_json.to_json()
-        ).to_json()
-
-        pb = payload.Payload()
-
-        pb.add("e", "ue")
-        pb.add_json(envelope, self.encode_base64, "ue_px", "ue_pr", self.json_encoder)
-
-        self.complete_payload(pb, context, tstamp, event_subject)
+        sd = SelfDescribing(
+            event_json=event_json,
+            context=context,
+            true_timestamp=tstamp,
+            event_subject=event_subject,
+        )
+        self.track(
+            event=sd,
+        )
         return self
 
     # Alias
@@ -902,7 +968,7 @@ class Tracker:
         event_json: SelfDescribingJson,
         context: Optional[List[SelfDescribingJson]] = None,
         tstamp: Optional[float] = None,
-        event_subject: Optional[_subject.Subject] = None,
+        event_subject: Optional[Subject] = None,
     ) -> "Tracker":
         """
         :param  event_json:      The properties of the event. Has two field:
@@ -922,7 +988,13 @@ class Tracker:
             DeprecationWarning,
             stacklevel=2,
         )
-        self.track_self_describing_event(event_json, context, tstamp, event_subject)
+
+        self.track_self_describing_event(
+            event_json=event_json,
+            context=context,
+            true_timestamp=tstamp,
+            event_subject=event_subject,
+        )
         return self
 
     def flush(self, is_async: bool = False) -> "Tracker":
@@ -942,7 +1014,7 @@ class Tracker:
                     emitter.sync_flush()
         return self
 
-    def set_subject(self, subject: Optional[_subject.Subject]) -> "Tracker":
+    def set_subject(self, subject: Optional[Subject]) -> "Tracker":
         """
         Set the subject of the events fired by the tracker
 


Test patch:
diff --git a/snowplow_tracker/test/integration/test_integration.py b/snowplow_tracker/test/integration/test_integration.py
index f9323a1..a95e10a 100644
--- a/snowplow_tracker/test/integration/test_integration.py
+++ b/snowplow_tracker/test/integration/test_integration.py
@@ -105,14 +105,15 @@ class IntegrationTest(unittest.TestCase):
         t = tracker.Tracker("namespace", [get_emitter], default_subject)
         with HTTMock(pass_response_content):
             t.track_ecommerce_transaction(
-                "6a8078be",
-                35,
+                order_id="6a8078be",
+                total_value=35,
                 city="London",
                 currency="GBP",
                 items=[
                     {"sku": "pbz0026", "price": 20, "quantity": 1},
                     {"sku": "pbz0038", "price": 15, "quantity": 1},
                 ],
+                tstamp=1399021242240,
             )
 
         expected_fields = {
@@ -161,7 +162,7 @@ class IntegrationTest(unittest.TestCase):
             "namespace", [get_emitter], default_subject, encode_base64=False
         )
         with HTTMock(pass_response_content):
-            t.track_mobile_screen_view("534", "Game HUD 2")
+            t.track_mobile_screen_view(id_="534", name="Game HUD 2")
         expected_fields = {"e": "ue"}
         for key in expected_fields:
             self.assertEqual(
@@ -520,6 +521,7 @@ class IntegrationTest(unittest.TestCase):
             t.track_struct_event("Test", "A")  # 322 bytes
             t.track_struct_event("Test", "A")  # 483 bytes. Send
             t.track_struct_event("Test", "AA")  # 162
+
         self.assertEqual(len(querystrings[-1]["data"]), 3)
         self.assertEqual(default_emitter.bytes_queued, 156 + len(_version.__version__))
 
@@ -548,7 +550,7 @@ class IntegrationTest(unittest.TestCase):
         self.assertEqual(actual_a, unicode_a)
 
         uepr_string = unquote_plus(from_querystring("ue_pr", querystrings[-1]))
-        actual_b = json.loads(uepr_string)["data"]["data"]["id"]
+        actual_b = json.loads(uepr_string)["data"]["data"]["name"]
         self.assertEqual(actual_b, unicode_b)
 
     def test_unicode_post(self) -> None:
@@ -571,5 +573,5 @@ class IntegrationTest(unittest.TestCase):
         self.assertEqual(in_test_ctx, unicode_a)
 
         sv_event = querystrings[-1]
-        in_uepr_name = json.loads(sv_event["data"][0]["ue_pr"])["data"]["data"]["id"]
+        in_uepr_name = json.loads(sv_event["data"][0]["ue_pr"])["data"]["data"]["name"]
         self.assertEqual(in_uepr_name, unicode_b)
diff --git a/snowplow_tracker/test/unit/test_event.py b/snowplow_tracker/test/unit/test_event.py
new file mode 100644
index 0000000..e50da98
--- /dev/null
+++ b/snowplow_tracker/test/unit/test_event.py
@@ -0,0 +1,72 @@
+# """
+#     test_event.py
+
+#     Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+
+#     This program is licensed to you under the Apache License Version 2.0,
+#     and you may not use this file except in compliance with the Apache License
+#     Version 2.0. You may obtain a copy of the Apache License Version 2.0 at
+#     http://www.apache.org/licenses/LICENSE-2.0.
+
+#     Unless required by applicable law or agreed to in writing,
+#     software distributed under the Apache License Version 2.0 is distributed on
+#     an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+#     express or implied. See the Apache License Version 2.0 for the specific
+#     language governing permissions and limitations there under.
+# """
+
+import json
+import unittest
+from snowplow_tracker.events import Event
+from snowplow_tracker.subject import Subject
+from snowplow_tracker.self_describing_json import SelfDescribingJson
+
+CONTEXT_SCHEMA = "iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-1"
+
+
+class TestEvent(unittest.TestCase):
+    def setUp(self) -> None:
+        pass
+
+    def test_init(self):
+        event = Event()
+        self.assertEqual(event.payload.nv_pairs, {})
+
+    def test_build_payload(self):
+        event_subject = Subject()
+        event = Event(event_subject=event_subject)
+        payload = event.build_payload(encode_base64=None, json_encoder=None)
+
+        self.assertEqual(payload.nv_pairs, {"p": "pc"})
+
+    def test_build_payload_tstamp(self):
+        event_subject = Subject()
+        tstamp = 1399021242030
+
+        event = Event(event_subject=event_subject, true_timestamp=tstamp)
+
+        payload = event.build_payload(
+            json_encoder=None,
+            encode_base64=None,
+        )
+
+        self.assertEqual(payload.nv_pairs, {"p": "pc", "ttm": 1399021242030})
+
+    def test_build_payload_context(self):
+        event_subject = Subject()
+        context = SelfDescribingJson("test.context.schema", {"user": "tester"})
+        event_context = [context]
+        event = Event(event_subject=event_subject, context=event_context)
+
+        payload = event.build_payload(
+            json_encoder=None,
+            encode_base64=False,
+        )
+
+        expected_context = {
+            "schema": CONTEXT_SCHEMA,
+            "data": [{"schema": "test.context.schema", "data": {"user": "tester"}}],
+        }
+        actual_context = json.loads(payload.nv_pairs["co"])
+
+        self.assertDictEqual(actual_context, expected_context)
diff --git a/snowplow_tracker/test/unit/test_tracker.py b/snowplow_tracker/test/unit/test_tracker.py
index 8e6cb6b..8eaf989 100644
--- a/snowplow_tracker/test/unit/test_tracker.py
+++ b/snowplow_tracker/test/unit/test_tracker.py
@@ -21,7 +21,7 @@ import unittest
 import unittest.mock as mock
 
 from freezegun import freeze_time
-from typing import Any
+from typing import Any, Optional
 
 from snowplow_tracker.contracts import disable_contracts, enable_contracts
 from snowplow_tracker.tracker import Tracker
@@ -29,6 +29,7 @@ from snowplow_tracker.tracker import VERSION as TRACKER_VERSION
 from snowplow_tracker.subject import Subject
 from snowplow_tracker.payload import Payload
 from snowplow_tracker.self_describing_json import SelfDescribingJson
+from snowplow_tracker.events import Event, SelfDescribing, ScreenView
 
 UNSTRUCT_SCHEMA = "iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0"
 CONTEXT_SCHEMA = "iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-1"
@@ -57,11 +58,21 @@ def mocked_uuid() -> str:
     return _TEST_UUID
 
 
-def mocked_track(pb: Any) -> None:
+def mocked_track(
+    event: Any,
+    context: Optional[Any] = None,
+    tstamp: Optional[Any] = None,
+    event_subject: Optional[Any] = None,
+) -> None:
     pass
 
 
-def mocked_complete_payload(*args: Any, **kwargs: Any) -> None:
+def mocked_complete_payload(
+    event: Any,
+    event_subject: Optional[Any],
+    context: Optional[Any],
+    tstamp: Optional[Any],
+) -> Payload:
     pass
 
 
@@ -223,122 +234,49 @@ class TestTracker(unittest.TestCase):
 
         t = Tracker("namespace", [e1, e2, e3])
 
-        p = Payload({"eid": "event_id"})
-        event_id = t.track(p)
+        mok_event = self.create_patch("snowplow_tracker.events.Event")
+        t.track(mok_event)
+        mok_payload = mok_event.build_payload().nv_pairs
 
-        e1.input.assert_called_once_with({"eid": "event_id"})
-        e2.input.assert_called_once_with({"eid": "event_id"})
-        e3.input.assert_called_once_with({"eid": "event_id"})
-
-        self.assertEqual(event_id, "event_id")
+        e1.input.assert_called_once_with(mok_payload)
+        e2.input.assert_called_once_with(mok_payload)
+        e3.input.assert_called_once_with(mok_payload)
 
     @freeze_time("2021-04-19 00:00:01")  # unix: 1618790401000
-    @mock.patch("snowplow_tracker.Tracker.track")
     @mock.patch("snowplow_tracker.Tracker.get_uuid")
-    def test_complete_payload(self, mok_uuid: Any, mok_track: Any) -> None:
-        mokEmitter = self.create_patch("snowplow_tracker.Emitter")
-        e = mokEmitter()
-
+    def test_complete_payload(self, mok_uuid: Any) -> None:
         mok_uuid.side_effect = mocked_uuid
-        mok_track.side_effect = mocked_track
-
-        t = Tracker("namespace", e)
-        p = Payload()
-        t.complete_payload(p, None, None, None)
-
-        self.assertEqual(mok_track.call_count, 1)
-        trackArgsTuple = mok_track.call_args_list[0][0]
-        self.assertEqual(len(trackArgsTuple), 1)
-        passed_nv_pairs = trackArgsTuple[0].nv_pairs
-
-        expected = {
-            "eid": _TEST_UUID,
-            "dtm": 1618790401000,
-            "tv": TRACKER_VERSION,
-            "p": "pc",
-            "tna": "namespace",
-        }
-        self.assertDictEqual(passed_nv_pairs, expected)
 
-    @freeze_time("2021-04-19 00:00:01")  # unix: 1618790401000
-    @mock.patch("snowplow_tracker.Tracker.track")
-    @mock.patch("snowplow_tracker.Tracker.get_uuid")
-    def test_complete_payload_tstamp_int(self, mok_uuid: Any, mok_track: Any) -> None:
         mokEmitter = self.create_patch("snowplow_tracker.Emitter")
         e = mokEmitter()
 
-        mok_uuid.side_effect = mocked_uuid
-        mok_track.side_effect = mocked_track
-
         t = Tracker("namespace", e)
-        p = Payload()
-        time_in_millis = 100010001000
-        t.complete_payload(p, None, time_in_millis, None)
-
-        self.assertEqual(mok_track.call_count, 1)
-        trackArgsTuple = mok_track.call_args_list[0][0]
-        self.assertEqual(len(trackArgsTuple), 1)
-        passed_nv_pairs = trackArgsTuple[0].nv_pairs
+        s = Subject()
+        event = Event(event_subject=s)
+        payload = t.complete_payload(event).nv_pairs
 
         expected = {
-            "tna": "namespace",
             "eid": _TEST_UUID,
             "dtm": 1618790401000,
-            "ttm": time_in_millis,
             "tv": TRACKER_VERSION,
             "p": "pc",
-        }
-        self.assertDictEqual(passed_nv_pairs, expected)
-
-    @freeze_time("2021-04-19 00:00:01")  # unix: 1618790401000
-    @mock.patch("snowplow_tracker.Tracker.track")
-    @mock.patch("snowplow_tracker.Tracker.get_uuid")
-    def test_complete_payload_tstamp_dtm(self, mok_uuid: Any, mok_track: Any) -> None:
-        mokEmitter = self.create_patch("snowplow_tracker.Emitter")
-        e = mokEmitter()
-
-        mok_uuid.side_effect = mocked_uuid
-        mok_track.side_effect = mocked_track
-
-        t = Tracker("namespace", e)
-        p = Payload()
-        time_in_millis = 100010001000
-        t.complete_payload(p, None, time_in_millis, None)
-
-        self.assertEqual(mok_track.call_count, 1)
-        trackArgsTuple = mok_track.call_args_list[0][0]
-        self.assertEqual(len(trackArgsTuple), 1)
-        passed_nv_pairs = trackArgsTuple[0].nv_pairs
-
-        expected = {
             "tna": "namespace",
-            "eid": _TEST_UUID,
-            "dtm": 1618790401000,
-            "ttm": time_in_millis,
-            "tv": TRACKER_VERSION,
-            "p": "pc",
         }
-        self.assertDictEqual(passed_nv_pairs, expected)
+        self.assertDictEqual(payload, expected)
 
     @freeze_time("2021-04-19 00:00:01")  # unix: 1618790401000
-    @mock.patch("snowplow_tracker.Tracker.track")
     @mock.patch("snowplow_tracker.Tracker.get_uuid")
-    def test_complete_payload_tstamp_ttm(self, mok_uuid: Any, mok_track: Any) -> None:
+    def test_complete_payload_tstamp(self, mok_uuid: Any) -> None:
         mokEmitter = self.create_patch("snowplow_tracker.Emitter")
         e = mokEmitter()
 
         mok_uuid.side_effect = mocked_uuid
-        mok_track.side_effect = mocked_track
-
         t = Tracker("namespace", e)
-        p = Payload()
+        s = Subject()
         time_in_millis = 100010001000
-        t.complete_payload(p, None, time_in_millis, None)
+        event = Event(true_timestamp=time_in_millis, event_subject=s)
 
-        self.assertEqual(mok_track.call_count, 1)
-        trackArgsTuple = mok_track.call_args_list[0][0]
-        self.assertEqual(len(trackArgsTuple), 1)
-        passed_nv_pairs = trackArgsTuple[0].nv_pairs
+        payload = t.complete_payload(event=event).nv_pairs
 
         expected = {
             "tna": "namespace",
@@ -348,30 +286,23 @@ class TestTracker(unittest.TestCase):
             "tv": TRACKER_VERSION,
             "p": "pc",
         }
-        self.assertDictEqual(passed_nv_pairs, expected)
+        self.assertDictEqual(payload, expected)
 
     @freeze_time("2021-04-19 00:00:01")  # unix: 1618790401000
-    @mock.patch("snowplow_tracker.Tracker.track")
     @mock.patch("snowplow_tracker.Tracker.get_uuid")
-    def test_complete_payload_co(self, mok_uuid: Any, mok_track: Any) -> None:
+    def test_complete_payload_co(self, mok_uuid: Any) -> None:
         mokEmitter = self.create_patch("snowplow_tracker.Emitter")
         e = mokEmitter()
 
         mok_uuid.side_effect = mocked_uuid
-        mok_track.side_effect = mocked_track
 
         t = Tracker("namespace", e, encode_base64=False)
-        p = Payload()
 
         geo_ctx = SelfDescribingJson(geoSchema, geoData)
         mov_ctx = SelfDescribingJson(movSchema, movData)
         ctx_array = [geo_ctx, mov_ctx]
-        t.complete_payload(p, ctx_array, None, None)
-
-        self.assertEqual(mok_track.call_count, 1)
-        trackArgsTuple = mok_track.call_args_list[0][0]
-        self.assertEqual(len(trackArgsTuple), 1)
-        passed_nv_pairs = trackArgsTuple[0].nv_pairs
+        event = Event(context=ctx_array)
+        payload = t.complete_payload(event=event).nv_pairs
 
         expected_co = {
             "schema": CONTEXT_SCHEMA,
@@ -380,55 +311,39 @@ class TestTracker(unittest.TestCase):
                 {"schema": movSchema, "data": movData},
             ],
         }
-        self.assertIn("co", passed_nv_pairs)
-        self.assertDictEqual(json.loads(passed_nv_pairs["co"]), expected_co)
+        self.assertIn("co", payload)
+        self.assertDictEqual(json.loads(payload["co"]), expected_co)
 
     @freeze_time("2021-04-19 00:00:01")  # unix: 1618790401000
-    @mock.patch("snowplow_tracker.Tracker.track")
     @mock.patch("snowplow_tracker.Tracker.get_uuid")
-    def test_complete_payload_cx(self, mok_uuid: Any, mok_track: Any) -> None:
+    def test_complete_payload_cx(self, mok_uuid: Any) -> None:
         mokEmitter = self.create_patch("snowplow_tracker.Emitter")
         e = mokEmitter()
 
         mok_uuid.side_effect = mocked_uuid
-        mok_track.side_effect = mocked_track
 
         t = Tracker("namespace", e, encode_base64=True)
-        p = Payload()
 
         geo_ctx = SelfDescribingJson(geoSchema, geoData)
         mov_ctx = SelfDescribingJson(movSchema, movData)
         ctx_array = [geo_ctx, mov_ctx]
-        t.complete_payload(p, ctx_array, None, None)
-
-        self.assertEqual(mok_track.call_count, 1)
-        trackArgsTuple = mok_track.call_args_list[0][0]
-        self.assertEqual(len(trackArgsTuple), 1)
-        passed_nv_pairs = trackArgsTuple[0].nv_pairs
+        event = Event(context=ctx_array)
+        payload = t.complete_payload(event=event).nv_pairs
 
-        self.assertIn("cx", passed_nv_pairs)
+        self.assertIn("cx", payload)
 
     @freeze_time("2021-04-19 00:00:01")  # unix: 1618790401000
-    @mock.patch("snowplow_tracker.Tracker.track")
     @mock.patch("snowplow_tracker.Tracker.get_uuid")
-    def test_complete_payload_event_subject(
-        self, mok_uuid: Any, mok_track: Any
-    ) -> None:
+    def test_complete_payload_event_subject(self, mok_uuid: Any) -> None:
         mokEmitter = self.create_patch("snowplow_tracker.Emitter")
         e = mokEmitter()
 
         mok_uuid.side_effect = mocked_uuid
-        mok_track.side_effect = mocked_track
 
         t = Tracker("namespace", e)
-        p = Payload()
-        evSubject = Subject().set_lang("EN").set_user_id("tester")
-        t.complete_payload(p, None, None, evSubject)
-
-        self.assertEqual(mok_track.call_count, 1)
-        trackArgsTuple = mok_track.call_args_list[0][0]
-        self.assertEqual(len(trackArgsTuple), 1)
-        passed_nv_pairs = trackArgsTuple[0].nv_pairs
+        event_subject = Subject().set_lang("EN").set_user_id("tester")
+        event = Event(event_subject=event_subject)
+        payload = t.complete_payload(event=event).nv_pairs
 
         expected = {
             "tna": "namespace",
@@ -439,72 +354,70 @@ class TestTracker(unittest.TestCase):
             "lang": "EN",
             "uid": "tester",
         }
-        self.assertDictEqual(passed_nv_pairs, expected)
+        self.assertDictEqual(payload, expected)
 
     ###
     # test track_x methods
     ###
 
-    @mock.patch("snowplow_tracker.Tracker.complete_payload")
-    def test_track_self_describing_event(self, mok_complete_payload: Any) -> None:
+    @mock.patch("snowplow_tracker.Tracker.track")
+    def test_track_self_describing_event(self, mok_track: Any) -> None:
         mokEmitter = self.create_patch("snowplow_tracker.Emitter")
         e = mokEmitter()
 
-        mok_complete_payload.side_effect = mocked_complete_payload
+        mok_track.side_effect = mocked_track
 
         t = Tracker("namespace", e, encode_base64=False)
-        evJson = SelfDescribingJson("test.sde.schema", {"n": "v"})
-        t.track_self_describing_event(evJson)
-        self.assertEqual(mok_complete_payload.call_count, 1)
-        completeArgsList = mok_complete_payload.call_args_list[0][0]
-        self.assertEqual(len(completeArgsList), 4)
+        event_json = SelfDescribingJson("test.sde.schema", {"n": "v"})
+        event = SelfDescribing(event_json=event_json)
+        actual_pairs = event.build_payload(
+            encode_base64=t.encode_base64,
+            json_encoder=t.json_encoder,
+        ).nv_pairs
+
+        t.track_self_describing_event(event_json)
+        self.assertEqual(mok_track.call_count, 1)
+
+        complete_args_dict = mok_track.call_args_list[0][1]
+        self.assertEqual(len(complete_args_dict), 1)
 
         # payload
-        actualPayloadArg = completeArgsList[0]
-        actualPairs = actualPayloadArg.nv_pairs
-        actualUePr = json.loads(actualPairs["ue_pr"])
-        # context
-        actualContextArg = completeArgsList[1]
-        # tstamp
-        actualTstampArg = completeArgsList[2]
+        actual_ue_pr = json.loads(actual_pairs["ue_pr"])
 
         expectedUePr = {
             "data": {"data": {"n": "v"}, "schema": "test.sde.schema"},
             "schema": UNSTRUCT_SCHEMA,
         }
 
-        self.assertDictEqual(actualUePr, expectedUePr)
-        self.assertEqual(actualPairs["e"], "ue")
-        self.assertTrue(actualContextArg is None)
-        self.assertTrue(actualTstampArg is None)
+        self.assertDictEqual(actual_ue_pr, expectedUePr)
+        self.assertEqual(actual_pairs["e"], "ue")
 
-    @mock.patch("snowplow_tracker.Tracker.complete_payload")
-    def test_track_self_describing_event_all_args(
-        self, mok_complete_payload: Any
-    ) -> None:
+    @mock.patch("snowplow_tracker.Tracker.track")
+    def test_track_self_describing_event_all_args(self, mok_track: Any) -> None:
         mokEmitter = self.create_patch("snowplow_tracker.Emitter")
         e = mokEmitter()
 
-        mok_complete_payload.side_effect = mocked_complete_payload
+        mok_track.side_effect = mocked_track
 
         t = Tracker("namespace", e, encode_base64=False)
-        evJson = SelfDescribingJson("test.schema", {"n": "v"})
+        event_json = SelfDescribingJson("test.schema", {"n": "v"})
         ctx = SelfDescribingJson("test.context.schema", {"user": "tester"})
-        evContext = [ctx]
-        evTstamp = 1399021242030
-        t.track_self_describing_event(evJson, evContext, evTstamp)
-        self.assertEqual(mok_complete_payload.call_count, 1)
-        completeArgsList = mok_complete_payload.call_args_list[0][0]
-        self.assertEqual(len(completeArgsList), 4)
+        event_context = [ctx]
+        event_tstamp = 1399021242030
+
+        event = SelfDescribing(event_json=event_json)
+        actual_pairs = event.build_payload(
+            encode_base64=t.encode_base64,
+            json_encoder=t.json_encoder,
+        ).nv_pairs
+
+        t.track_self_describing_event(event_json, event_context, event_tstamp)
+        self.assertEqual(mok_track.call_count, 1)
+        complete_args_dict = mok_track.call_args_list[0][1]
+        self.assertEqual(len(complete_args_dict), 1)
 
         # payload
-        actualPayloadArg = completeArgsList[0]
-        actualPairs = actualPayloadArg.nv_pairs
-        actualUePr = json.loads(actualPairs["ue_pr"])
-        # context
-        actualContextArg = completeArgsList[1]
-        # tstamp
-        actualTstampArg = completeArgsList[2]
+        actualUePr = json.loads(actual_pairs["ue_pr"])
 
         expectedUePr = {
             "data": {"data": {"n": "v"}, "schema": "test.schema"},
@@ -512,40 +425,40 @@ class TestTracker(unittest.TestCase):
         }
 
         self.assertDictEqual(actualUePr, expectedUePr)
-        self.assertEqual(actualPairs["e"], "ue")
-        self.assertIs(actualContextArg[0], ctx)
-        self.assertEqual(actualTstampArg, evTstamp)
+        self.assertEqual(actual_pairs["e"], "ue")
 
-    @mock.patch("snowplow_tracker.Tracker.complete_payload")
-    def test_track_self_describing_event_encode(
-        self, mok_complete_payload: Any
-    ) -> None:
+    @mock.patch("snowplow_tracker.Tracker.track")
+    def test_track_self_describing_event_encode(self, mok_track: Any) -> None:
         mokEmitter = self.create_patch("snowplow_tracker.Emitter")
         e = mokEmitter()
 
-        mok_complete_payload.side_effect = mocked_complete_payload
+        mok_track.side_effect = mocked_track
 
         t = Tracker("namespace", e, encode_base64=True)
-        evJson = SelfDescribingJson("test.sde.schema", {"n": "v"})
-        t.track_self_describing_event(evJson)
-        self.assertEqual(mok_complete_payload.call_count, 1)
-        completeArgsList = mok_complete_payload.call_args_list[0][0]
-        self.assertEqual(len(completeArgsList), 4)
+        event_json = SelfDescribingJson("test.sde.schema", {"n": "v"})
 
-        actualPayloadArg = completeArgsList[0]
-        actualPairs = actualPayloadArg.nv_pairs
-        self.assertTrue("ue_px" in actualPairs.keys())
+        event = SelfDescribing(event_json=event_json)
+        actual_pairs = event.build_payload(
+            encode_base64=t.encode_base64,
+            json_encoder=t.json_encoder,
+        ).nv_pairs
 
-    @mock.patch("snowplow_tracker.Tracker.complete_payload")
-    def test_track_struct_event(self, mok_complete_payload: Any) -> None:
+        t.track_self_describing_event(event_json)
+        self.assertEqual(mok_track.call_count, 1)
+        complete_args_dict = mok_track.call_args_list[0][1]
+        self.assertEqual(len(complete_args_dict), 1)
+        self.assertTrue("ue_px" in actual_pairs.keys())
+
+    @mock.patch("snowplow_tracker.Tracker.track")
+    def test_track_struct_event(self, mok_track: Any) -> None:
         mokEmitter = self.create_patch("snowplow_tracker.Emitter")
         e = mokEmitter()
 
-        mok_complete_payload.side_effect = mocked_complete_payload
+        mok_track.side_effect = mocked_track
 
         t = Tracker("namespace", e)
         ctx = SelfDescribingJson("test.context.schema", {"user": "tester"})
-        evTstamp = 1399021242030
+        ev_tstamp = 1399021242030
         t.track_struct_event(
             "Mixes",
             "Play",
@@ -553,18 +466,16 @@ class TestTracker(unittest.TestCase):
             "TestProp",
             value=3.14,
             context=[ctx],
-            tstamp=evTstamp,
+            tstamp=ev_tstamp,
         )
-        self.assertEqual(mok_complete_payload.call_count, 1)
-        completeArgsList = mok_complete_payload.call_args_list[0][0]
-        self.assertEqual(len(completeArgsList), 4)
+        self.assertEqual(mok_track.call_count, 1)
+        complete_args_dict = mok_track.call_args_list[0][1]
+        self.assertEqual(len(complete_args_dict), 1)
 
-        actualPayloadArg = completeArgsList[0]
-        actualContextArg = completeArgsList[1]
-        actualTstampArg = completeArgsList[2]
-        actualPairs = actualPayloadArg.nv_pairs
+        actual_payload_arg = complete_args_dict["event"].payload
+        actual_pairs = actual_payload_arg.nv_pairs
 
-        expectedPairs = {
+        expected_pairs = {
             "e": "se",
             "se_ca": "Mixes",
             "se_ac": "Play",
@@ -572,31 +483,31 @@ class TestTracker(unittest.TestCase):
             "se_pr": "TestProp",
             "se_va": 3.14,
         }
-        self.assertDictEqual(actualPairs, expectedPairs)
-        self.assertIs(actualContextArg[0], ctx)
-        self.assertEqual(actualTstampArg, evTstamp)
+        self.assertDictEqual(actual_pairs, expected_pairs)
 
-    @mock.patch("snowplow_tracker.Tracker.complete_payload")
-    def test_track_page_view(self, mok_complete_payload: Any) -> None:
+    @mock.patch("snowplow_tracker.Tracker.track")
+    def test_track_page_view(self, mok_track: Any) -> None:
         mokEmitter = self.create_patch("snowplow_tracker.Emitter")
         e = mokEmitter()
 
-        mok_complete_payload.side_effect = mocked_complete_payload
+        mok_track.side_effect = mocked_track
 
         t = Tracker("namespace", e)
         ctx = SelfDescribingJson("test.context.schema", {"user": "tester"})
-        evTstamp = 1399021242030
+        ev_tstamp = 1399021242030
         t.track_page_view(
-            "example.com", "Example", "docs.snowplow.io", context=[ctx], tstamp=evTstamp
+            "example.com",
+            "Example",
+            "docs.snowplow.io",
+            context=[ctx],
+            tstamp=ev_tstamp,
         )
-        self.assertEqual(mok_complete_payload.call_count, 1)
-        completeArgsList = mok_complete_payload.call_args_list[0][0]
-        self.assertEqual(len(completeArgsList), 4)
+        self.assertEqual(mok_track.call_count, 1)
+        complete_args_dict = mok_track.call_args_list[0][1]
+        self.assertEqual(len(complete_args_dict), 1)
 
-        actualPayloadArg = completeArgsList[0]
-        actualContextArg = completeArgsList[1]
-        actualTstampArg = completeArgsList[2]
-        actualPairs = actualPayloadArg.nv_pairs
+        actual_payload_arg = complete_args_dict["event"].payload
+        actualPairs = actual_payload_arg.nv_pairs
 
         expectedPairs = {
             "e": "pv",
@@ -605,19 +516,17 @@ class TestTracker(unittest.TestCase):
             "refr": "docs.snowplow.io",
         }
         self.assertDictEqual(actualPairs, expectedPairs)
-        self.assertIs(actualContextArg[0], ctx)
-        self.assertEqual(actualTstampArg, evTstamp)
 
-    @mock.patch("snowplow_tracker.Tracker.complete_payload")
-    def test_track_page_ping(self, mok_complete_payload: Any) -> None:
+    @mock.patch("snowplow_tracker.Tracker.track")
+    def test_track_page_ping(self, mok_track: Any) -> None:
         mokEmitter = self.create_patch("snowplow_tracker.Emitter")
         e = mokEmitter()
 
-        mok_complete_payload.side_effect = mocked_complete_payload
+        mok_track.side_effect = mocked_track
 
         t = Tracker("namespace", e)
         ctx = SelfDescribingJson("test.context.schema", {"user": "tester"})
-        evTstamp = 1399021242030
+        ev_tstamp = 1399021242030
         t.track_page_ping(
             "example.com",
             "Example",
@@ -627,16 +536,14 @@ class TestTracker(unittest.TestCase):
             2,
             3,
             context=[ctx],
-            tstamp=evTstamp,
+            tstamp=ev_tstamp,
         )
-        self.assertEqual(mok_complete_payload.call_count, 1)
-        completeArgsList = mok_complete_payload.call_args_list[0][0]
-        self.assertEqual(len(completeArgsList), 4)
+        self.assertEqual(mok_track.call_count, 1)
+        complete_args_dict = mok_track.call_args_list[0][1]
+        self.assertEqual(len(complete_args_dict), 1)
 
-        actualPayloadArg = completeArgsList[0]
-        actualContextArg = completeArgsList[1]
-        actualTstampArg = completeArgsList[2]
-        actualPairs = actualPayloadArg.nv_pairs
+        actual_payload_arg = complete_args_dict["event"].payload
+        actual_pairs = actual_payload_arg.nv_pairs
 
         expectedPairs = {
             "e": "pp",
@@ -648,39 +555,35 @@ class TestTracker(unittest.TestCase):
             "pp_miy": 2,
             "pp_may": 3,
         }
-        self.assertDictEqual(actualPairs, expectedPairs)
-        self.assertIs(actualContextArg[0], ctx)
-        self.assertEqual(actualTstampArg, evTstamp)
+        self.assertDictEqual(actual_pairs, expectedPairs)
 
-    @mock.patch("snowplow_tracker.Tracker.complete_payload")
-    def test_track_ecommerce_transaction_item(self, mok_complete_payload: Any) -> None:
+    @mock.patch("snowplow_tracker.Tracker.track")
+    def test_track_ecommerce_transaction_item(self, mok_track: Any) -> None:
         mokEmitter = self.create_patch("snowplow_tracker.Emitter")
         e = mokEmitter()
 
-        mok_complete_payload.side_effect = mocked_complete_payload
+        mok_track.side_effect = mocked_track
 
         t = Tracker("namespace", e)
         ctx = SelfDescribingJson("test.context.schema", {"user": "tester"})
-        evTstamp = 1399021242030
+        ev_tstamp = 1399021242030
         t.track_ecommerce_transaction_item(
-            "1234",
-            "sku1234",
-            3.14,
-            1,
-            "itemName",
-            "itemCategory",
-            "itemCurrency",
+            order_id="1234",
+            sku="sku1234",
+            price=3.14,
+            quantity=1,
+            name="itemName",
+            category="itemCategory",
+            currency="itemCurrency",
             context=[ctx],
-            tstamp=evTstamp,
+            tstamp=ev_tstamp,
         )
-        self.assertEqual(mok_complete_payload.call_count, 1)
-        completeArgsList = mok_complete_payload.call_args_list[0][0]
-        self.assertEqual(len(completeArgsList), 4)
+        self.assertEqual(mok_track.call_count, 1)
+        complete_args_list = mok_track.call_args_list[0][1]
+        self.assertEqual(len(complete_args_list), 1)
 
-        actualPayloadArg = completeArgsList[0]
-        actualContextArg = completeArgsList[1]
-        actualTstampArg = completeArgsList[2]
-        actualPairs = actualPayloadArg.nv_pairs
+        actual_payload_arg = complete_args_list["event"].payload
+        actual_pairs = actual_payload_arg.nv_pairs
 
         expectedPairs = {
             "e": "ti",
@@ -692,18 +595,14 @@ class TestTracker(unittest.TestCase):
             "ti_qu": 1,
             "ti_cu": "itemCurrency",
         }
-        self.assertDictEqual(actualPairs, expectedPairs)
-        self.assertIs(actualContextArg[0], ctx)
-        self.assertEqual(actualTstampArg, evTstamp)
+        self.assertDictEqual(actual_pairs, expectedPairs)
 
-    @mock.patch("snowplow_tracker.Tracker.complete_payload")
-    def test_track_ecommerce_transaction_no_items(
-        self, mok_complete_payload: Any
-    ) -> None:
+    @mock.patch("snowplow_tracker.Tracker.track")
+    def test_track_ecommerce_transaction_no_items(self, mok_track: Any) -> None:
         mokEmitter = self.create_patch("snowplow_tracker.Emitter")
         e = mokEmitter()
 
-        mok_complete_payload.side_effect = mocked_complete_payload
+        mok_track.side_effect = mocked_track
 
         t = Tracker("namespace", e)
         ctx = SelfDescribingJson("test.context.schema", {"user": "tester"})
@@ -721,12 +620,11 @@ class TestTracker(unittest.TestCase):
             context=[ctx],
             tstamp=evTstamp,
         )
-        self.assertEqual(mok_complete_payload.call_count, 1)
-        completeArgsList = mok_complete_payload.call_args_list[0][0]
-        self.assertEqual(len(completeArgsList), 4)
-        actualPayloadArg = completeArgsList[0]
-        actualContextArg = completeArgsList[1]
-        actualTstampArg = completeArgsList[2]
+        self.assertEqual(mok_track.call_count, 1)
+        completeArgsList = mok_track.call_args_list[0][1]
+        self.assertEqual(len(completeArgsList), 1)
+
+        actualPayloadArg = completeArgsList["event"].payload
         actualPairs = actualPayloadArg.nv_pairs
 
         expectedPairs = {
@@ -742,18 +640,16 @@ class TestTracker(unittest.TestCase):
             "tr_cu": "transCurrency",
         }
         self.assertDictEqual(actualPairs, expectedPairs)
-        self.assertIs(actualContextArg[0], ctx)
-        self.assertEqual(actualTstampArg, evTstamp)
 
     @mock.patch("snowplow_tracker.Tracker.track_ecommerce_transaction_item")
-    @mock.patch("snowplow_tracker.Tracker.complete_payload")
+    @mock.patch("snowplow_tracker.Tracker.track")
     def test_track_ecommerce_transaction_with_items(
-        self, mok_complete_payload: Any, mok_track_trans_item: Any
+        self, mok_track: Any, mok_track_trans_item: Any
     ) -> None:
         mokEmitter = self.create_patch("snowplow_tracker.Emitter")
         e = mokEmitter()
 
-        mok_complete_payload.side_effect = mocked_complete_payload
+        mok_track.side_effect = mocked_track
         mok_track_trans_item.side_effect = mocked_track_trans_item
 
         t = Tracker("namespace", e)
@@ -764,27 +660,26 @@ class TestTracker(unittest.TestCase):
             {"sku": "sku5678", "quantity": 1, "price": 2.72},
         ]
         t.track_ecommerce_transaction(
-            "1234",
-            10,
-            "transAffiliation",
-            2.5,
-            1.5,
-            "transCity",
-            "transState",
-            "transCountry",
-            "transCurrency",
+            order_id="1234",
+            total_value=10,
+            affiliation="transAffiliation",
+            tax_value=2.5,
+            shipping=1.5,
+            city="transCity",
+            state="transState",
+            country="transCountry",
+            currency="transCurrency",
             items=transItems,
             context=[ctx],
             tstamp=evTstamp,
         )
 
         # Transaction
-        callCompleteArgsList = mok_complete_payload.call_args_list
-        firstCallArgsList = callCompleteArgsList[0][0]
-        self.assertEqual(len(firstCallArgsList), 4)
-        actualPayloadArg = firstCallArgsList[0]
-        actualContextArg = firstCallArgsList[1]
-        actualTstampArg = firstCallArgsList[2]
+        callCompleteArgsList = mok_track.call_args_list
+        firstCallArgsList = callCompleteArgsList[0][1]
+        self.assertEqual(len(firstCallArgsList), 1)
+
+        actualPayloadArg = firstCallArgsList["event"].payload
         actualPairs = actualPayloadArg.nv_pairs
 
         expectedTransPairs = {
@@ -800,8 +695,6 @@ class TestTracker(unittest.TestCase):
             "tr_cu": "transCurrency",
         }
         self.assertDictEqual(actualPairs, expectedTransPairs)
-        self.assertIs(actualContextArg[0], ctx)
-        self.assertEqual(actualTstampArg, evTstamp)
 
         # Items
         calls_to_track_trans_item = mok_track_trans_item.call_count
@@ -813,14 +706,16 @@ class TestTracker(unittest.TestCase):
         firstItemCallKwargs = callTrackItemsArgsList[0][1]
 
         expectedFirstItemPairs = {
-            "tstamp": evTstamp,
-            "order_id": "1234",
-            "currency": "transCurrency",
             "sku": "sku1234",
             "quantity": 3,
             "price": 3.14,
+            "order_id": "1234",
+            "currency": "transCurrency",
+            "tstamp": evTstamp,
             "event_subject": None,
+            "context": [ctx],
         }
+
         self.assertDictEqual(firstItemCallKwargs, expectedFirstItemPairs)
         # 2nd item
         secItemCallArgs = callTrackItemsArgsList[1][0]
@@ -828,14 +723,16 @@ class TestTracker(unittest.TestCase):
         secItemCallKwargs = callTrackItemsArgsList[1][1]
 
         expectedSecItemPairs = {
-            "tstamp": evTstamp,
-            "order_id": "1234",
-            "currency": "transCurrency",
             "sku": "sku5678",
             "quantity": 1,
             "price": 2.72,
+            "order_id": "1234",
+            "currency": "transCurrency",
+            "tstamp": evTstamp,
             "event_subject": None,
+            "context": [ctx],
         }
+
         self.assertDictEqual(secItemCallKwargs, expectedSecItemPairs)
 
     @mock.patch("snowplow_tracker.Tracker.track_self_describing_event")
@@ -870,11 +767,11 @@ class TestTracker(unittest.TestCase):
             },
         }
 
-        callArgs = mok_track_unstruct.call_args_list[0][0]
+        callArgs = mok_track_unstruct.call_args_list[0][1]
         self.assertEqual(len(callArgs), 4)
-        self.assertDictEqual(callArgs[0].to_json(), expected)
-        self.assertIs(callArgs[1][0], ctx)
-        self.assertEqual(callArgs[2], evTstamp)
+        self.assertDictEqual(callArgs["event_json"].to_json(), expected)
+        self.assertIs(callArgs["context"][0], ctx)
+        self.assertEqual(callArgs["true_timestamp"], evTstamp)
 
     @mock.patch("snowplow_tracker.Tracker.track_self_describing_event")
     def test_track_link_click_optional_none(self, mok_track_unstruct: Any) -> None:
@@ -894,11 +791,11 @@ class TestTracker(unittest.TestCase):
             },
         }
 
-        callArgs = mok_track_unstruct.call_args_list[0][0]
+        callArgs = mok_track_unstruct.call_args_list[0][1]
         self.assertEqual(len(callArgs), 4)
-        self.assertDictEqual(callArgs[0].to_json(), expected)
-        self.assertTrue(callArgs[1] is None)
-        self.assertTrue(callArgs[2] is None)
+        self.assertDictEqual(callArgs["event_json"].to_json(), expected)
+        self.assertTrue(callArgs["context"] is None)
+        self.assertTrue(callArgs["true_timestamp"] is None)
 
     @mock.patch("snowplow_tracker.Tracker.track_self_describing_event")
     def test_track_add_to_cart(self, mok_track_unstruct: Any) -> None:
@@ -934,11 +831,11 @@ class TestTracker(unittest.TestCase):
             },
         }
 
-        callArgs = mok_track_unstruct.call_args_list[0][0]
+        callArgs = mok_track_unstruct.call_args_list[0][1]
         self.assertEqual(len(callArgs), 4)
-        self.assertDictEqual(callArgs[0].to_json(), expected)
-        self.assertIs(callArgs[1][0], ctx)
-        self.assertEqual(callArgs[2], evTstamp)
+        self.assertDictEqual(callArgs["event_json"].to_json(), expected)
+        self.assertIs(callArgs["context"][0], ctx)
+        self.assertEqual(callArgs["true_timestamp"], evTstamp)
 
     @mock.patch("snowplow_tracker.Tracker.track_self_describing_event")
     def test_track_add_to_cart_optional_none(self, mok_track_unstruct: Any) -> None:
@@ -956,11 +853,11 @@ class TestTracker(unittest.TestCase):
             "data": {"sku": "sku1234", "quantity": 1},
         }
 
-        callArgs = mok_track_unstruct.call_args_list[0][0]
+        callArgs = mok_track_unstruct.call_args_list[0][1]
         self.assertEqual(len(callArgs), 4)
-        self.assertDictEqual(callArgs[0].to_json(), expected)
-        self.assertTrue(callArgs[1] is None)
-        self.assertTrue(callArgs[2] is None)
+        self.assertDictEqual(callArgs["event_json"].to_json(), expected)
+        self.assertTrue(callArgs["context"] is None)
+        self.assertTrue(callArgs["true_timestamp"] is None)
 
     @mock.patch("snowplow_tracker.Tracker.track_self_describing_event")
     def test_track_remove_from_cart(self, mok_track_unstruct: Any) -> None:
@@ -996,11 +893,11 @@ class TestTracker(unittest.TestCase):
             },
         }
 
-        callArgs = mok_track_unstruct.call_args_list[0][0]
+        callArgs = mok_track_unstruct.call_args_list[0][1]
         self.assertEqual(len(callArgs), 4)
-        self.assertDictEqual(callArgs[0].to_json(), expected)
-        self.assertIs(callArgs[1][0], ctx)
-        self.assertEqual(callArgs[2], evTstamp)
+        self.assertDictEqual(callArgs["event_json"].to_json(), expected)
+        self.assertIs(callArgs["context"][0], ctx)
+        self.assertEqual(callArgs["true_timestamp"], evTstamp)
 
     @mock.patch("snowplow_tracker.Tracker.track_self_describing_event")
     def test_track_remove_from_cart_optional_none(
@@ -1020,11 +917,11 @@ class TestTracker(unittest.TestCase):
             "data": {"sku": "sku1234", "quantity": 1},
         }
 
-        callArgs = mok_track_unstruct.call_args_list[0][0]
+        callArgs = mok_track_unstruct.call_args_list[0][1]
         self.assertEqual(len(callArgs), 4)
-        self.assertDictEqual(callArgs[0].to_json(), expected)
-        self.assertTrue(callArgs[1] is None)
-        self.assertTrue(callArgs[2] is None)
+        self.assertDictEqual(callArgs["event_json"].to_json(), expected)
+        self.assertTrue(callArgs["context"] is None)
+        self.assertTrue(callArgs["true_timestamp"] is None)
 
     @mock.patch("snowplow_tracker.Tracker.track_self_describing_event")
     def test_track_form_change(self, mok_track_unstruct: Any) -> None:
@@ -1060,11 +957,11 @@ class TestTracker(unittest.TestCase):
             },
         }
 
-        callArgs = mok_track_unstruct.call_args_list[0][0]
+        callArgs = mok_track_unstruct.call_args_list[0][1]
         self.assertEqual(len(callArgs), 4)
-        self.assertDictEqual(callArgs[0].to_json(), expected)
-        self.assertIs(callArgs[1][0], ctx)
-        self.assertEqual(callArgs[2], evTstamp)
+        self.assertDictEqual(callArgs["event_json"].to_json(), expected)
+        self.assertIs(callArgs["context"][0], ctx)
+        self.assertEqual(callArgs["true_timestamp"], evTstamp)
 
     @mock.patch("snowplow_tracker.Tracker.track_self_describing_event")
     def test_track_form_change_optional_none(self, mok_track_unstruct: Any) -> None:
@@ -1086,11 +983,11 @@ class TestTracker(unittest.TestCase):
             },
         }
 
-        callArgs = mok_track_unstruct.call_args_list[0][0]
+        callArgs = mok_track_unstruct.call_args_list[0][1]
         self.assertEqual(len(callArgs), 4)
-        self.assertDictEqual(callArgs[0].to_json(), expected)
-        self.assertTrue(callArgs[1] is None)
-        self.assertTrue(callArgs[2] is None)
+        self.assertDictEqual(callArgs["event_json"].to_json(), expected)
+        self.assertTrue(callArgs["context"] is None)
+        self.assertTrue(callArgs["true_timestamp"] is None)
 
     @mock.patch("snowplow_tracker.Tracker.track_self_describing_event")
     def test_track_form_submit(self, mok_track_unstruct: Any) -> None:
@@ -1128,11 +1025,11 @@ class TestTracker(unittest.TestCase):
             },
         }
 
-        callArgs = mok_track_unstruct.call_args_list[0][0]
+        callArgs = mok_track_unstruct.call_args_list[0][1]
         self.assertEqual(len(callArgs), 4)
-        self.assertDictEqual(callArgs[0].to_json(), expected)
-        self.assertIs(callArgs[1][0], ctx)
-        self.assertEqual(callArgs[2], evTstamp)
+        self.assertDictEqual(callArgs["event_json"].to_json(), expected)
+        self.assertIs(callArgs["context"][0], ctx)
+        self.assertEqual(callArgs["true_timestamp"], evTstamp)
 
     @mock.patch("snowplow_tracker.Tracker.track_self_describing_event")
     def test_track_form_submit_invalid_element_type(
@@ -1203,11 +1100,11 @@ class TestTracker(unittest.TestCase):
             },
         }
 
-        callArgs = mok_track_unstruct.call_args_list[0][0]
+        callArgs = mok_track_unstruct.call_args_list[0][1]
         self.assertEqual(len(callArgs), 4)
-        self.assertDictEqual(callArgs[0].to_json(), expected)
-        self.assertIs(callArgs[1][0], ctx)
-        self.assertEqual(callArgs[2], evTstamp)
+        self.assertDictEqual(callArgs["event_json"].to_json(), expected)
+        self.assertIs(callArgs["context"][0], ctx)
+        self.assertEqual(callArgs["true_timestamp"], evTstamp)
 
     @mock.patch("snowplow_tracker.Tracker.track_self_describing_event")
     def test_track_form_submit_optional_none(self, mok_track_unstruct: Any) -> None:
@@ -1221,11 +1118,11 @@ class TestTracker(unittest.TestCase):
 
         expected = {"schema": FORM_SUBMIT_SCHEMA, "data": {"formId": "testFormId"}}
 
-        callArgs = mok_track_unstruct.call_args_list[0][0]
+        callArgs = mok_track_unstruct.call_args_list[0][1]
         self.assertEqual(len(callArgs), 4)
-        self.assertDictEqual(callArgs[0].to_json(), expected)
-        self.assertTrue(callArgs[1] is None)
-        self.assertTrue(callArgs[2] is None)
+        self.assertDictEqual(callArgs["event_json"].to_json(), expected)
+        self.assertTrue(callArgs["context"] is None)
+        self.assertTrue(callArgs["true_timestamp"] is None)
 
     @mock.patch("snowplow_tracker.Tracker.track_self_describing_event")
     def test_track_form_submit_empty_elems(self, mok_track_unstruct: Any) -> None:
@@ -1239,9 +1136,9 @@ class TestTracker(unittest.TestCase):
 
         expected = {"schema": FORM_SUBMIT_SCHEMA, "data": {"formId": "testFormId"}}
 
-        callArgs = mok_track_unstruct.call_args_list[0][0]
+        callArgs = mok_track_unstruct.call_args_list[0][1]
         self.assertEqual(len(callArgs), 4)
-        self.assertDictEqual(callArgs[0].to_json(), expected)
+        self.assertDictEqual(callArgs["event_json"].to_json(), expected)
 
     @mock.patch("snowplow_tracker.Tracker.track_self_describing_event")
     def test_track_site_search(self, mok_track_unstruct: Any) -> None:
@@ -1268,11 +1165,12 @@ class TestTracker(unittest.TestCase):
             },
         }
 
-        callArgs = mok_track_unstruct.call_args_list[0][0]
+        callArgs = mok_track_unstruct.call_args_list[0][1]
+
         self.assertEqual(len(callArgs), 4)
-        self.assertDictEqual(callArgs[0].to_json(), expected)
-        self.assertIs(callArgs[1][0], ctx)
-        self.assertEqual(callArgs[2], evTstamp)
+        self.assertDictEqual(callArgs["event_json"].to_json(), expected)
+        self.assertIs(callArgs["context"][0], ctx)
+        self.assertEqual(callArgs["true_timestamp"], evTstamp)
 
     @mock.patch("snowplow_tracker.Tracker.track_self_describing_event")
     def test_track_site_search_optional_none(self, mok_track_unstruct: Any) -> None:
@@ -1289,37 +1187,42 @@ class TestTracker(unittest.TestCase):
             "data": {"terms": ["track", "search"]},
         }
 
-        callArgs = mok_track_unstruct.call_args_list[0][0]
+        callArgs = mok_track_unstruct.call_args_list[0][1]
         self.assertEqual(len(callArgs), 4)
-        self.assertDictEqual(callArgs[0].to_json(), expected)
-        self.assertTrue(callArgs[1] is None)
-        self.assertTrue(callArgs[2] is None)
+        self.assertDictEqual(callArgs["event_json"].to_json(), expected)
+        self.assertTrue(callArgs["context"] is None)
+        self.assertTrue(callArgs["true_timestamp"] is None)
 
-    @mock.patch("snowplow_tracker.Tracker.track_self_describing_event")
-    def test_track_mobile_screen_view(self, mok_track_unstruct: Any) -> None:
+    @mock.patch("snowplow_tracker.Tracker.track")
+    def test_track_mobile_screen_view(self, mok_track: Any) -> None:
         mokEmitter = self.create_patch("snowplow_tracker.Emitter")
         e = mokEmitter()
 
-        mok_track_unstruct.side_effect = mocked_track_unstruct
+        mok_track.side_effect = mocked_track
 
         t = Tracker("namespace", e)
-        ctx = SelfDescribingJson("test.context.schema", {"user": "tester"})
-        evTstamp = 1399021242030
 
-        t.track_mobile_screen_view(
-            "screenId", "screenName", context=[ctx], tstamp=evTstamp
-        )
+        screen_view = ScreenView(name="screenName", id_="screenId")
+        actual_pairs = screen_view.build_payload(
+            encode_base64=False,
+            json_encoder=t.json_encoder,
+        ).nv_pairs
+
+        t.track(screen_view)
+
+        self.assertEqual(mok_track.call_count, 1)
+        complete_args_dict = mok_track.call_args_list[0][0]
+        self.assertEqual(len(complete_args_dict), 1)
+        actual_ue_pr = json.loads(actual_pairs["ue_pr"])
 
         expected = {
             "schema": MOBILE_SCREEN_VIEW_SCHEMA,
-            "data": {"name": "screenName", "id": "screenId"},
+            "data": {"id": "screenId", "name": "screenName"},
         }
 
-        callArgs = mok_track_unstruct.call_args_list[0][0]
-        self.assertEqual(len(callArgs), 4)
-        self.assertDictEqual(callArgs[0].to_json(), expected)
-        self.assertIs(callArgs[1][0], ctx)
-        self.assertEqual(callArgs[2], evTstamp)
+        complete_args_dict = mok_track.call_args_list[0][1]
+        complete_args_dict = mok_track.call_args_list[0][1]
+        self.assertDictEqual(actual_ue_pr["data"], expected)
 
     @mock.patch("snowplow_tracker.Tracker.track_self_describing_event")
     def test_track_screen_view(self, mok_track_unstruct: Any) -> None:
@@ -1339,8 +1242,8 @@ class TestTracker(unittest.TestCase):
             "data": {"name": "screenName", "id": "screenId"},
         }
 
-        callArgs = mok_track_unstruct.call_args_list[0][0]
+        callArgs = mok_track_unstruct.call_args_list[0][1]
         self.assertEqual(len(callArgs), 4)
-        self.assertDictEqual(callArgs[0].to_json(), expected)
-        self.assertIs(callArgs[1][0], ctx)
-        self.assertEqual(callArgs[2], evTstamp)
+        self.assertDictEqual(callArgs["event_json"].to_json(), expected)
+        self.assertIs(callArgs["context"][0], ctx)
+        self.assertEqual(callArgs["true_timestamp"], evTstamp)